- config and host editing pretty much done
authorcbarratt <cbarratt>
Tue, 24 Jan 2006 07:10:56 +0000 (07:10 +0000)
committercbarratt <cbarratt>
Tue, 24 Jan 2006 07:10:56 +0000 (07:10 +0000)
 - added charset conversion

 - utf8 support pretty much done

 - added more hardlink limit checking

 - added scripts bin/BackupPC_attribPrint, bin/BackupPC_fixupBackupSummary
   and bin/BackupPC_tarPCCopy

30 files changed:
bin/BackupPC
bin/BackupPC_attribPrint [new file with mode: 0755]
bin/BackupPC_dump
bin/BackupPC_fixupBackupSummary [new file with mode: 0755]
bin/BackupPC_link
bin/BackupPC_nightly
bin/BackupPC_tarCreate
bin/BackupPC_tarExtract
bin/BackupPC_tarPCCopy [new file with mode: 0755]
bin/BackupPC_zipCreate
conf/BackupPC_stnd.css
conf/config.pl
configure.pl
lib/BackupPC/CGI/EditConfig.pm
lib/BackupPC/CGI/Lib.pm
lib/BackupPC/CGI/StartStopBackup.pm
lib/BackupPC/Config/Meta.pm
lib/BackupPC/FileZIO.pm
lib/BackupPC/Lang/en.pm
lib/BackupPC/Lang/fr.pm
lib/BackupPC/Lang/it.pm
lib/BackupPC/Lib.pm
lib/BackupPC/PoolWrite.pm
lib/BackupPC/Storage.pm
lib/BackupPC/Storage/Text.pm
lib/BackupPC/View.pm
lib/BackupPC/Xfer/Rsync.pm
lib/BackupPC/Xfer/RsyncFileIO.pm
lib/BackupPC/Xfer/Tar.pm
makeDist

index 4c13633..2a81d11 100755 (executable)
@@ -416,9 +416,6 @@ sub Main_TryToRun_nightly
 
        #
        # Zero out the data we expect to get from BackupPC_nightly.
-       # In the future if we want to split BackupPC_nightly over
-       # more than one night we will only zero out the portion
-       # that we are running right now.
        #
        for my $p ( qw(pool cpool) ) {
            for ( my $i = $start ; $i < $end ; $i++ ) {
@@ -442,7 +439,7 @@ sub Main_TryToRun_nightly
 
        #
        # Now queue the $Conf{MaxBackupPCNightlyJobs} jobs.
-       # The granularity on start and end is now 0..256.
+       # The granularity on start and end is now 0..255.
        #
        $start *= 16;
        $end   *= 16;
@@ -470,7 +467,6 @@ sub Main_TryToRun_nightly
             $CmdQueueOn{$job} = 1;
         }
         $RunNightlyWhenIdle = 2;
-
     }
 }
 
diff --git a/bin/BackupPC_attribPrint b/bin/BackupPC_attribPrint
new file mode 100755 (executable)
index 0000000..47ecf79
--- /dev/null
@@ -0,0 +1,70 @@
+#!/bin/perl
+#============================================================= -*-perl-*-
+#
+# BackupPC_attribPrint: print the contents of attrib files.
+#
+# DESCRIPTION
+#  
+#   Usage: BackupPC_attribPrint attribPath
+#
+#   Compression status of attrib path is based on $Conf{CompressLevel}.
+#
+# AUTHOR
+#   Craig Barratt  <cbarratt@users.sourceforge.net>
+#
+# COPYRIGHT
+#   Copyright (C) 2005  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
+#   the Free Software Foundation; either version 2 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program; if not, write to the Free Software
+#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+#========================================================================
+#
+# Version 2.1.0, released 20 Jun 2004.
+#
+# See http://backuppc.sourceforge.net.
+#
+#========================================================================
+
+use strict;
+no  utf8;
+use lib "/usr/local/BackupPC/lib";
+
+use BackupPC::Lib;
+use BackupPC::Attrib qw(:all);
+use BackupPC::FileZIO;
+use Data::Dumper;
+
+die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
+my $TopDir = $bpc->TopDir();
+my $BinDir = $bpc->BinDir();
+my %Conf   = $bpc->Conf();
+
+if ( @ARGV != 1 ) {
+    print STDERR "Usage: $0 attribPath\n";
+    exit(1);
+}
+if ( !-f $ARGV[0] ) {
+    print STDERR "$ARGV[0] does not exist\n";
+    exit(1);
+}
+
+my $attrib = BackupPC::Attrib->new({ compress => $Conf{CompressLevel} });
+if ( !$attrib->read(".", $ARGV[0]) ) {
+    print STDERR "Cannot read attrib file $ARGV[0]\n";
+    exit(1);
+}
+my $info = $attrib->get();
+$Data::Dumper::Indent = 1;
+print Dumper($info);
index 8151cbd..ef29ed8 100755 (executable)
@@ -81,6 +81,7 @@ no  utf8;
 use lib "/usr/local/BackupPC/lib";
 use BackupPC::Lib;
 use BackupPC::FileZIO;
+use BackupPC::Storage;
 use BackupPC::Xfer::Smb;
 use BackupPC::Xfer::Tar;
 use BackupPC::Xfer::Rsync;
@@ -1064,7 +1065,8 @@ sub BackupExpire
     my($client) = @_;
     my($Dir) = "$TopDir/pc/$client";
     my(@Backups) = $bpc->BackupInfoRead($client);
-    my($cntFull, $cntIncr, $firstFull, $firstIncr, $oldestIncr, $oldestFull);
+    my($cntFull, $cntIncr, $firstFull, $firstIncr, $oldestIncr,
+       $oldestFull, $changes);
 
     if ( $Conf{FullKeepCnt} <= 0 ) {
         print(LOG $bpc->timeStamp,
@@ -1106,6 +1108,7 @@ sub BackupExpire
            print(LOG $bpc->timeStamp,
                       "removing incr backup $Backups[$firstIncr]{num}\n");
             BackupRemove($client, \@Backups, $firstIncr);
+            $changes++;
             next;
         }
 
@@ -1145,6 +1148,7 @@ sub BackupExpire
            print(LOG $bpc->timeStamp,
                    "removing old full backup $Backups[$firstFull]{num}\n");
             BackupRemove($client, \@Backups, $firstFull);
+            $changes++;
             next;
         }
 
@@ -1154,7 +1158,7 @@ sub BackupExpire
         #
         last if ( !BackupFullExpire($client, \@Backups) );
     }
-    $bpc->BackupInfoWrite($client, @Backups);
+    $bpc->BackupInfoWrite($client, @Backups) if ( $changes );
 }
 
 #
@@ -1248,6 +1252,7 @@ sub BackupSave
 {
     my @Backups = $bpc->BackupInfoRead($client);
     my $num  = -1;
+    my $newFilesFH;
 
     #
     # Since we got a good backup we should remove any partial dumps
@@ -1290,7 +1295,18 @@ sub BackupSave
     $Backups[$i]{noFill}        = $type eq "incr" ? 1 : 0;
     $Backups[$i]{level}         = $type eq "incr" ? 1 : 0;
     $Backups[$i]{mangle}        = 1;        # name mangling always on for v1.04+
+    $Backups[$i]{xferMethod}    = $Conf{XferMethod};
+    $Backups[$i]{charset}       = $Conf{ClientCharset};
+    #
+    # Save the main backups file
+    #
     $bpc->BackupInfoWrite($client, @Backups);
+    #
+    # Save just this backup's info in case the main backups file
+    # gets corrupted
+    #
+    BackupPC::Storage->backupInfoWrite($Dir, $Backups[$i]{num},
+                                             $Backups[$i]);
 
     unlink("$Dir/timeStamp.level0") if ( -f "$Dir/timeStamp.level0" );
     foreach my $ext ( qw(bad bad.z) ) {
@@ -1316,11 +1332,34 @@ sub BackupSave
                $file = "$f->{share}/$f->{file}";
            }
            next if ( !-f "$Dir/$Backups[$j]{num}/$file" );
-           if ( !link("$Dir/$Backups[$j]{num}/$file",
-                               "$Dir/$num/$shareM/$fileM") ) {
-               my $str = \"Unable to link $num/$f->{share}/$f->{file} to"
-                         . " $Backups[$j]{num}/$f->{share}/$f->{file}\n";
-               $XferLOG->write(\$str);
+
+            my($exists, $digest, $origSize, $outSize, $errs)
+                                = BackupPC::PoolWrite::LinkOrCopy(
+                                      $bpc,
+                                      "$Dir/$Backups[$j]{num}/$file",
+                                      $Backups[$j]{compress},
+                                      "$Dir/$num/$shareM/$fileM",
+                                      $Conf{CompressLevel});
+            if ( !$exists ) {
+                #
+                # the hard link failed, most likely because the target
+                # file has too many links.  We have copied the file
+                # instead, so add this to the new file list.
+                #
+                if ( !defined($newFilesFH) ) {
+                    my $str = "Appending to NewFileList for $shareM/$fileM\n";
+                    $XferLOG->write(\$str);
+                    open($newFilesFH, ">>", "$TopDir/pc/$client/NewFileList")
+                         || die("can't open $TopDir/pc/$client/NewFileList");
+                    binmode($newFilesFH);
+                }
+                if ( -f "$Dir/$num/$shareM/$fileM" ) {
+                    print($newFilesFH "$digest $origSize $shareM/$fileM\n");
+                } else {
+                    my $str = "Unable to link/copy $num/$f->{share}/$f->{file}"
+                            . " to $Backups[$j]{num}/$f->{share}/$f->{file}\n";
+                    $XferLOG->write(\$str);
+                }
            } else {
                my $str = "Bad file $num/$f->{share}/$f->{file} replaced"
                         . " by link to"
@@ -1335,6 +1374,7 @@ sub BackupSave
            $XferLOG->write(\$str);
        }
     }
+    close($newFilesFH) if ( defined($newFilesFH) );
     $XferLOG->close();
     rename("$Dir/XferLOG$fileExt", "$Dir/XferLOG.$num$fileExt");
     rename("$Dir/NewFileList", "$Dir/NewFileList.$num");
diff --git a/bin/BackupPC_fixupBackupSummary b/bin/BackupPC_fixupBackupSummary
new file mode 100755 (executable)
index 0000000..5bc5318
--- /dev/null
@@ -0,0 +1,251 @@
+#!/bin/perl
+#============================================================= -*-perl-*-
+#
+# BackupPC_fixupBackupSummary: recreate backups file in case
+# it was lost.
+#
+# DESCRIPTION
+#  
+#   Usage: BackupPC_fixupBackupSummary [clients...]
+#
+# AUTHOR
+#   Craig Barratt  <cbarratt@users.sourceforge.net>
+#
+# COPYRIGHT
+#   Copyright (C) 2005  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
+#   the Free Software Foundation; either version 2 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program; if not, write to the Free Software
+#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+#========================================================================
+#
+# Version 2.1.0, released 20 Jun 2004.
+#
+# See http://backuppc.sourceforge.net.
+#
+#========================================================================
+
+use strict;
+no  utf8;
+use lib "/usr/local/BackupPC/lib";
+use Getopt::Std;
+use Data::Dumper;
+use Time::ParseDate;
+
+use BackupPC::Lib;
+use BackupPC::Attrib qw(:all);
+use BackupPC::FileZIO;
+use BackupPC::Storage;
+
+die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
+my $TopDir = $bpc->TopDir();
+my $BinDir = $bpc->BinDir();
+my %Conf   = $bpc->Conf();
+my $Hosts  = $bpc->HostInfoRead();
+my @hostList;
+
+our(%backupInfo);
+
+if ( !@ARGV ) {
+    @hostList = sort(keys(%$Hosts));
+} else {
+    @hostList = @ARGV;
+}
+
+foreach my $host ( @hostList ) {
+    my(@Backups, $BkupFromLOG, $BkupFromInfo, $BkupNums, @LogFiles);
+
+    $BkupFromInfo = {};
+    $BkupFromLOG  = {};
+    if ( !defined($Hosts->{$host}) ) {
+        print("$host doesn't exist in BackupPC's host file... skipping\n");
+        next;
+    }
+
+    my $dir = "$TopDir/pc/$host";
+    print("Doing host $host\n");
+
+    if ( !opendir(DIR, $dir) ) {
+        print("$host: Can't open $dir... skipping $host\n");
+        next;
+    }
+
+    #
+    # Read the backups file
+    #
+    @Backups = $bpc->BackupInfoRead($host);
+
+    #
+    # Temporary: create backupInfo files in each backup
+    # directory
+    #
+    foreach ( my $i = 0 ; $i < @Backups ; $i++ ) {
+        BackupPC::Storage->backupInfoWrite($dir, $Backups[$i]{num},
+                                           $Backups[$i]);
+        if ( 0 ) {
+            my $bkupNum = $Backups[$i]{num};
+            if ( !-f "$dir/$bkupNum/backupInfo" ) {
+                my($dump) = Data::Dumper->new(
+                         [   $Backups[$i]],
+                         [qw(*backupInfo)]);
+                $dump->Indent(1);
+                if ( open(BKUPINFO, ">", "$dir/$bkupNum/backupInfo") ) {
+                    print(BKUPINFO $dump->Dump);
+                    close(BKUPINFO);
+                }
+            }
+        }
+    }
+
+    #
+    # Look through the LOG files to get information about
+    # completed backups.  The data from the LOG file is
+    # incomplete, but enough to get some useful info.
+    #
+    # Also, try to pick up the new-style of information
+    # that is kept in each backup tree.  This info is
+    # complete.  This data is only saved after version
+    # 2.1.2.
+    #
+    my @files = readdir(DIR);
+    closedir(DIR);
+    foreach my $file ( @files ) {
+        if ( $file =~ /^LOG(.\d+\.z)?/ ) {
+            push(@LogFiles, $file);
+        } elsif ( $file =~ /^(\d+)$/ ) {
+            my $bkupNum = $1;
+            $BkupNums->{$bkupNum} = 1;
+
+            next if ( !-f "$dir/$bkupNum/backupInfo" );
+
+            #
+            # Read backup info
+            #
+            %backupInfo = ();
+            print("    Reading $dir/$bkupNum/backupInfo\n");
+            if ( !(my $ret = do "$dir/$bkupNum/backupInfo") ) {
+                print("    couldn't parse $dir/$bkupNum/backupInfo: $@\n") if $@;
+                print("    couldn't do $dir/$bkupNum/backupInfo: $!\n")
+                                                        unless defined $ret;
+                print("    couldn't run $dir/$bkupNum/backupInfo\n");
+                next;
+            }
+            if ( !keys(%backupInfo) || !defined($backupInfo{num}) ) {
+                print("    $dir/$bkupNum/backupInfo is empty\n");
+                next;
+            }
+            %{$BkupFromInfo->{$backupInfo{num}}} = %backupInfo;
+        }
+    }
+
+    #
+    # Read through LOG files from oldest to newest
+    #
+    @LogFiles = sort({-M "$dir/$a" <=> -M "$dir/$b"} @LogFiles);
+    my $startTime;
+    foreach my $file ( @LogFiles ) {
+        my $f = BackupPC::FileZIO->open("$dir/$file", 0, $file =~ /\.z/);
+
+        if ( !defined($f) ) {
+            print("$host: unable to open file $dir/$file\n");
+            next;
+        }
+        print("    Reading $file\n");
+        while ( (my $str = $f->readLine()) ne "" ) {
+            if ( $str =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (full|incr|partial) backup started for directory / ) {
+                $startTime = $str;
+                next;
+            }
+            next if ( $str !~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (full|incr|partial) backup (\d+) complete, (\d+) files, (\d+) bytes, (\d+) xferErrs \((\d+) bad files, (\d+) bad shares, (\d+) other\)/ );
+
+            my $endTime     = $1;
+            my $type        = $2;
+            my $bkupNum     = $3;
+            my $nFilesTotal = $4;
+            my $sizeTotal   = $5;
+            my $xferErrs    = $6;
+            my $badFiles    = $7;
+            my $badShare    = $8;
+            print("    Got $type backup $bkupNum at $endTime\n");
+            next if ( !-d "$dir/$2" );
+            $BkupFromLOG->{$bkupNum} = {
+                num            => $bkupNum,
+                type           => $type,
+                startTime      => parsedate($startTime),
+                endTime        => parsedate($endTime),
+                size           => $sizeTotal,
+                nFiles         => $nFilesTotal,
+                xferErrs       => $xferErrs,
+                xferBadFile    => $badFiles,
+                xferBadShare   => $badShare,
+                nFilesExist    => 0,
+                sizeExist      => 0,
+                sizeExistComp  => 0,
+                tarErrs        => 0,
+                compress       => $Conf{CompressLevel},
+                noFill         => $type eq "incr" ? 1 : 0,
+                level          => $type eq "incr" ? 1 : 0,
+                mangle         => 1,
+                noFill         => $noFill;
+                fillFromNum    => $fillFromNum;
+            };
+        }
+    }
+
+    splice(@Backups, 2, 1);
+    
+    #
+    # Now merge any info from $BkupFromInfo and $BkupFromLOG
+    # that is missing from @Backups.
+    #
+    # First, anything in @Backups overrides the other data
+    #
+    #
+    foreach ( my $i = 0 ; $i < @Backups ; $i++ ) {
+        my $bkupNum = $Backups[$i]{num};
+        delete($BkupFromLOG->{$bkupNum});
+        delete($BkupFromInfo->{$bkupNum});
+        delete($BkupNums->{$bkupNum});
+    }
+
+    #
+    # Now merge in data from the LOG and backupInfo files.
+    # backupInfo files override LOG files.
+    #
+    my $changes;
+
+    foreach my $bkupNum ( keys(%$BkupFromLOG) ) {
+        next if ( defined($BkupFromInfo->{$bkupNum}) );
+        print("    Adding info for backup $bkupNum from LOG file\n");
+        push(@Backups, $BkupFromLOG->{$bkupNum});
+        delete($BkupNums->{$bkupNum});
+        $changes++;
+    }
+    foreach my $bkupNum ( keys(%$BkupFromInfo) ) {
+        print("    Adding info for backup $bkupNum from backupInfo file\n");
+        push(@Backups, $BkupFromInfo->{$bkupNum});
+        delete($BkupNums->{$bkupNum});
+        $changes++;
+    }
+    foreach my $bkupNum ( keys(%$BkupNums) ) {
+        print("    *** No info for backup number $bkupNum\n");
+    }
+
+    if ( $changes ) {
+        @Backups = sort({$a->{num} <=> $b->{num}} @Backups);
+        print Dumper \@Backups;
+    } else {
+        print("    No changes for host $host\n");
+    }
+}
index c7e925d..b3cc51a 100755 (executable)
@@ -51,6 +51,7 @@ use lib "/usr/local/BackupPC/lib";
 use BackupPC::Lib;
 use BackupPC::Attrib;
 use BackupPC::PoolWrite;
+use BackupPC::Storage;
 
 use File::Find;
 use File::Path;
@@ -156,6 +157,16 @@ while ( 1 ) {
     $Backups[$num]{sizeNewComp}   += $sizeNewComp;
     $Backups[$num]{noFill}         = $noFill;
     $Backups[$num]{fillFromNum}    = $fillFromNum;
+    #
+    # Save just this backup's info in case the main backups file
+    # gets corrupted
+    #
+    BackupPC::Storage->backupInfoWrite($Dir,
+                                       $Backups[$num]{num},
+                                       $Backups[$num], 1);
+    #
+    # Save the main backups file
+    #
     $bpc->BackupInfoWrite($host, @Backups);
 }
 
@@ -211,7 +222,21 @@ sub FillIncr
         #
         # Exists in the older filled backup, and not in the new, so link it
         #
-        link($name, $newName);
+        my($exists, $digest, $origSize, $outSize, $errs)
+                    = BackupPC::PoolWrite::LinkOrCopy(
+                                      $bpc,
+                                      $name, $Compress,
+                                      $newName, $Compress);
+        if ( $exists ) {
+            $nFilesExist++;
+            $sizeExist += $origSize;
+            $sizeExistComp += $outSize;
+        } elsif ( $outSize > 0 ) {
+            $nFilesNew++;
+            $sizeNew += $origSize;
+            $sizeNewComp += -s $outSize;
+            LinkNewFile($digest, $origSize, $newName);
+        }
     }
 }
 
index 560115b..9f6f4eb 100755 (executable)
@@ -20,6 +20,9 @@
 #
 #     -m   Do monthly aging of per-PC log files and sending of email.
 #          Otherise, BackupPC_nightly just does pool pruning.
+#          Since several BackupPC_nightly processes might run
+#          concurrently, just the first one is given the -m flag
+#          by BackupPC.
 #
 #   The poolRangeStart and poolRangeEnd arguments are integers from 0 to 255.
 #   These specify which parts of the pool to process.  There are 256 2nd-level
@@ -208,11 +211,12 @@ for my $pool ( qw(pool cpool) ) {
 printf("BackupPC_nightly lock_off\n");
 
 ###########################################################################
-# Send email 
+# Send email and generation of backupInfo files for each backup
 ###########################################################################
 if ( $opts{m} ) {
     print("log BackupPC_nightly now running BackupPC_sendEmail\n");
-    system("$BinDir/BackupPC_sendEmail")
+    system("$BinDir/BackupPC_sendEmail");
+    doBackupInfoUpdate();
 }
 
 #
@@ -251,6 +255,31 @@ sub doPerPCLogFileAging
     }
 }
 
+#
+# Update the backupInfo files based on the backups file.
+# We do this just once a week (on Sun) since it is only
+# needed for old backups with BackupPC <= 2.1.2.
+#
+sub doBackupInfoUpdate
+{
+    my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
+    return if ( $wday != 0 );
+
+    foreach my $host ( sort(keys(%{$bpc->HostInfoRead()})) ) {
+        my @Backups = $bpc->BackupInfoRead($host);
+
+        for ( my $i = 0 ; $i < @Backups ; $i++ ) {
+            #
+            # BackupPC::Storage->backupInfoWrite won't overwrite
+            # an existing file
+            #
+            BackupPC::Storage->backupInfoWrite("$TopDir/pc/$host",
+                                               $Backups[$i]{num},
+                                               $Backups[$i]);
+        }
+    }
+}
+
 sub GetPoolStats
 {
     my($nlinks, $nblocks) = (lstat($_))[3, 12];
index ce67747..8d8d134 100755 (executable)
@@ -23,6 +23,8 @@
 #       -p pathAdd      new path prefix
 #       -b BLOCKS       BLOCKS x 512 bytes per record (default 20; same as tar)
 #       -w writeBufSz   write buffer size (default 1MB)
+#       -e charset      charset for encoding file names (default: value of
+#                       $Conf{ClientCharset} when backup was done)
 #
 #     The -h, -n and -s options specify which dump is used to generate
 #     the tar archive.  The -r and -p options can be used to relocate
@@ -62,19 +64,17 @@ no  utf8;
 use lib "/usr/local/BackupPC/lib";
 use File::Path;
 use Getopt::Std;
+use Encode qw/from_to/;
 use BackupPC::Lib;
 use BackupPC::Attrib qw(:all);
 use BackupPC::FileZIO;
 use BackupPC::View;
 
 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
-my $TopDir = $bpc->TopDir();
-my $BinDir = $bpc->BinDir();
-my %Conf   = $bpc->Conf();
 
 my %opts;
 
-if ( !getopts("th:n:p:r:s:b:w:", \%opts) || @ARGV < 1 ) {
+if ( !getopts("te:h:n:p:r:s:b:w:", \%opts) || @ARGV < 1 ) {
     print STDERR <<EOF;
 usage: $0 [options] files/directories...
   Required options:
@@ -90,6 +90,8 @@ usage: $0 [options] files/directories...
      -p pathAdd      new path prefix
      -b BLOCKS       BLOCKS x 512 bytes per record (default 20; same as tar)
      -w writeBufSz   write buffer size (default 1048576 = 1MB)
+     -e charset      charset for encoding file names (default: value of
+                     \$Conf{ClientCharset} when backup was done)
 EOF
     exit(1);
 }
@@ -123,12 +125,16 @@ if ( $i >= @Backups ) {
     exit(1);
 }
 
+my $Charset = $Backups[$i]{charset};
+$Charset = $opts{e} if ( $opts{e} ne "" );
+
 my $PathRemove = $1 if ( $opts{r} =~ /(.+)/ );
 my $PathAdd    = $1 if ( $opts{p} =~ /(.+)/ );
-if ( $opts{s} !~ /^([\w\s\.\/\$-]+)$/ && $opts{s} ne "*" ) {
+if ( $opts{s} !~ /^([\w\s.\/$(){}[\]-]+)$/ && $opts{s} ne "*" ) {
     print(STDERR "$0: bad share name '$opts{s}'\n");
     exit(1);
 }
+
 our $ShareName = $opts{s};
 our $view = BackupPC::View->new($bpc, $Host, \@Backups);
 
@@ -360,6 +366,14 @@ sub TarWriteFileInfo
 {
     my($fh, $hdr) = @_;
 
+    #
+    # Convert path names to requested (eg: client) charset
+    #
+    if ( $Charset ne "" ) {
+        from_to($hdr->{name},     "utf8", $Charset);
+        from_to($hdr->{linkname}, "utf8", $Charset);
+    }
+
     #
     # Handle long link names (symbolic links)
     #
@@ -373,6 +387,7 @@ sub TarWriteFileInfo
         TarWrite($fh, \$data);
         TarWritePad($fh, length($data));
     }
+
     #
     # Handle long file names
     #
index 32044f6..1686ffd 100755 (executable)
@@ -36,6 +36,7 @@
 use strict;
 no  utf8;
 use lib "/usr/local/BackupPC/lib";
+use Encode qw/from_to/;
 use BackupPC::Lib;
 use BackupPC::Attrib qw(:all);
 use BackupPC::FileZIO;
@@ -101,7 +102,7 @@ $SIG{TTIN} = \&catch_signal;
 #                 Copyright 1998 Stephen Zander. All rights reserved.
 #
 my $tar_unpack_header
-    = 'Z100 A8 A8 A8 A12 A12 A8 A1 Z100 A6 A2 Z32 Z32 A8 A8 A155 x12';
+    = 'Z100 A8 A8 A8 a12 A12 A8 A1 Z100 A6 A2 Z32 Z32 A8 A8 A155 x12';
 my $tar_header_length = 512;
 
 my $BufSize  = 1048576;     # 1MB or 2^20
@@ -250,8 +251,19 @@ sub TarReadFileInfo
                 $name, $mode, $size, $type) if ( $Conf{XferLogLevel} >= 3 );
         $name     = $longName if ( defined($longName) );
         $linkname = $longLink if ( defined($longLink) );
+
+        #
+        # Map client charset encodings to utf8
+        #
+        # printf("File $name (hex: %s)\n", unpack("H*", $name));
+        if ( $Conf{ClientCharset} ne "" ) {
+            from_to($name, $Conf{ClientCharset}, "utf8");
+            from_to($linkname, $Conf{ClientCharset}, "utf8");
+        }
+        printf("File now $name (hex: %s)\n", unpack("H*", $name));
+
         $name     =~ s{^\./+}{};
-        $name     =~ s{/+$}{};
+        $name     =~ s{/+\.?$}{};
         $name     =~ s{//+}{/}g;
         return {
             name       => $name,
@@ -313,7 +325,7 @@ sub TarReadFile
         #
         my($nRead);
         #print("Reading $f->{name}, $f->{size} bytes, type $f->{type}\n");
-        pathCreate($dir, "$OutDir/$ShareName/$f->{mangleName}", $file, $f);
+        pathCreate($dir, "$OutDir/$ShareName/$f->{mangleName}", $f);
         my $poolWrite = BackupPC::PoolWrite->new($bpc,
                                          "$OutDir/$ShareName/$f->{mangleName}",
                                          $f->{size}, $Compress);
@@ -351,7 +363,7 @@ sub TarReadFile
        # a plain file.
         #
         $f->{size} = length($f->{linkname});
-        pathCreate($dir, "$OutDir/$ShareName/$f->{mangleName}", $file, $f);
+        pathCreate($dir, "$OutDir/$ShareName/$f->{mangleName}", $f);
         my $poolWrite = BackupPC::PoolWrite->new($bpc,
                                          "$OutDir/$ShareName/$f->{mangleName}",
                                          $f->{size}, $Compress);
@@ -369,7 +381,7 @@ sub TarReadFile
         # contents.
         #
         $f->{size} = length($f->{linkname});
-        pathCreate($dir, "$OutDir/$ShareName/$f->{mangleName}", $file, $f);
+        pathCreate($dir, "$OutDir/$ShareName/$f->{mangleName}", $f);
         my $poolWrite = BackupPC::PoolWrite->new($bpc,
                                          "$OutDir/$ShareName/$f->{mangleName}",
                                          $f->{size}, $Compress);
@@ -393,7 +405,7 @@ sub TarReadFile
         } else {
             $data = "$f->{devmajor},$f->{devminor}";
         }
-        pathCreate($dir, "$OutDir/$ShareName/$f->{mangleName}", $file, $f);
+        pathCreate($dir, "$OutDir/$ShareName/$f->{mangleName}", $f);
         my $poolWrite = BackupPC::PoolWrite->new($bpc,
                                          "$OutDir/$ShareName/$f->{mangleName}",
                                          length($data), $Compress);
@@ -487,17 +499,21 @@ sub logFileAction
 #
 sub pathCreate
 {
-    my($dir, $fullPath, $file, $f) = @_;
+    my($dir, $fullPath, $f) = @_;
 
     #
     # Get parent directory of each of $dir and $fullPath
     #
-    $dir      =~ s{/[^/]*$}{};
+    # print("pathCreate: dir = $dir, fullPath = $fullPath\n");
+    $dir      =~ s{/([^/]*)$}{};
+    my $file  = $bpc->fileNameUnmangle($1);
     $fullPath =~ s{/[^/]*$}{};
-    return if ( -d $fullPath );
+    return if ( -d $fullPath || $file eq "" );
+    unlink($fullPath) if ( -e $fullPath );
     mkpath($fullPath, 0, 0777);
     $Attrib{$dir} = BackupPC::Attrib->new({ compress => $Compress })
                                 if ( !defined($Attrib{$dir}) );
+    # print("pathCreate: adding file = $file to dir = $dir\n");
     $Attrib{$dir}->set($file, {
                             type  => BPC_FTYPE_DIR,
                             mode  => 0755,
diff --git a/bin/BackupPC_tarPCCopy b/bin/BackupPC_tarPCCopy
new file mode 100755 (executable)
index 0000000..efb96a8
--- /dev/null
@@ -0,0 +1,550 @@
+#!/bin/perl
+#============================================================= -*-perl-*-
+#
+# BackupPC_tarPCCopy: create a tar archive of the PC directory
+# for copying the entire PC data directory.  The archive will
+# contain hardlinks to the pool directory, which should be copied
+# before BackupPC_tarPCCopy is run.
+#
+# DESCRIPTION
+#  
+#   Usage: BackupPC_tarPCCopy [options] files/directories...
+#
+#   Flags:
+#       -c      don't cache inode data (reduces memory usage at the
+#                                       expense of longer run time)
+#
+# AUTHOR
+#   Craig Barratt  <cbarratt@users.sourceforge.net>
+#
+# COPYRIGHT
+#   Copyright (C) 2005  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
+#   the Free Software Foundation; either version 2 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program; if not, write to the Free Software
+#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+#========================================================================
+#
+# Version 2.1.0, released 20 Jun 2004.
+#
+# See http://backuppc.sourceforge.net.
+#
+#========================================================================
+
+use strict;
+no  utf8;
+use lib "/usr/local/BackupPC/lib";
+use File::Find;
+use File::Path;
+use Getopt::Std;
+
+use BackupPC::Lib;
+use BackupPC::Attrib qw(:all);
+use BackupPC::FileZIO;
+use BackupPC::View;
+
+use constant S_IFMT       => 0170000;   # type of file
+
+die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
+my $TopDir = $bpc->TopDir();
+my $BinDir = $bpc->BinDir();
+my %Conf   = $bpc->Conf();
+
+my %opts;
+
+if ( !getopts("c", \%opts) || @ARGV < 1 ) {
+    print STDERR <<EOF;
+usage: $0 [options] files/directories...
+  Options:
+     -c      don't cache inode data (reduces memory usage at the
+                                     expense of longer run time)
+EOF
+    exit(1);
+}
+
+#
+# This constant and the line of code below that uses it are borrowed
+# from Archive::Tar.  Thanks to Calle Dybedahl and Stephen Zander.
+# See www.cpan.org.
+#
+# Archive::Tar is Copyright 1997 Calle Dybedahl. All rights reserved.
+#                 Copyright 1998 Stephen Zander. All rights reserved.
+#
+my $tar_pack_header
+    = 'a100 a8 a8 a8 a12 a12 A8 a1 a100 a6 a2 a32 a32 a8 a8 a155 x12';
+my $tar_header_length = 512;
+
+my $BufSize    = 1048576;               # 1MB or 2^20
+my $WriteBuf   = "";
+my $WriteBufSz = ($opts{b} || 20) * $tar_header_length;
+
+my(%UidCache, %GidCache);
+
+my($ClientName, $ClientBackups, $ClientBkupNum, $ClientDirAttr, $ClientDir);
+
+my $FileCnt    = 0;
+my $HLinkCnt   = 0;
+my $ByteCnt    = 0;        
+my $DirCnt     = 0;
+my $ErrorCnt   = 0;       
+my $ClientBkupCompress = 1;
+my $ClientBkupMangle   = 1;
+
+my %Inode2Path;
+
+#
+# Write out all the requested files/directories
+#
+binmode(STDOUT);
+my $fh = *STDOUT;
+
+my $argCnt = 1;
+my $argMax = @ARGV;
+
+while ( @ARGV ) {
+    my $path = shift(@ARGV);
+
+    if ( $path !~ m{^\Q$TopDir/\E} ) {
+        print STDERR "Argument $path must be an absolute path starting with $TopDir\n";
+        exit(1);
+    }
+    if ( !-d $path ) {
+        print STDERR "Argument $path does not exist\n";
+        exit(1);
+    }
+
+    find({wanted => sub { archiveFile($fh) } }, $path);
+
+    #
+    # To avoid using too much memory for the inode cache,
+    # remove it after each top-level directory is done.
+    #
+    %Inode2Path = ();
+
+    #
+    # Print some stats
+    #
+    print STDERR "Done $path ($argCnt of $argMax): $DirCnt dirs,"
+               . " $FileCnt files, $HLinkCnt hardlinks\n";
+
+    $FileCnt    = 0;
+    $HLinkCnt   = 0;
+    $ByteCnt    = 0;        
+    $DirCnt     = 0;
+
+    $argCnt++;
+}
+
+#
+# Finish with two null 512 byte headers, and then round out a full
+# block.
+# 
+my $data = "\0" x ($tar_header_length * 2);
+TarWrite($fh, \$data);
+TarWrite($fh, undef);
+
+if ( $ErrorCnt ) {
+    #
+    # Got errors so exit with a non-zero status
+    #
+    print STDERR "Got $ErrorCnt warnings/errors\n";
+    exit(1);
+}
+exit(0);
+
+###########################################################################
+# Subroutines
+###########################################################################
+
+sub archiveFile
+{
+    my($fh) = @_;
+    my($hdr);
+
+    my @s = stat($_);
+
+    #
+    # We just handle directories and files; no symlinks or
+    # char/block special files.
+    #
+    $hdr->{type}     = -d _ ? BPC_FTYPE_DIR
+                     : -f _ ? BPC_FTYPE_FILE
+                     : -1;
+    $hdr->{fullPath} = $File::Find::name;
+    $hdr->{inode}    = $s[1];
+    $hdr->{nlink}    = $s[3];
+    $hdr->{size}     = $s[7];
+    $hdr->{devmajor} = $s[6] >> 8;
+    $hdr->{devminor} = $s[6] & 0xff;
+    $hdr->{uid}      = $s[4];
+    $hdr->{gid}      = $s[5];
+    $hdr->{mode}     = $s[2];
+    $hdr->{mtime}    = $s[9];
+    $hdr->{compress} = 1;
+
+    if ( $hdr->{fullPath} !~ m{\Q$TopDir\E/pc/(.*)} ) {
+        print STDERR "Can't extract TopDir ($TopDir) from"
+                   . " $hdr->{fullPath}\n";
+        $ErrorCnt++;
+        return;
+    }
+    $hdr->{relPath}  = $1;
+    if ( $hdr->{relPath} =~ m{(.*)/(.*)} ) {
+        $hdr->{name} = $2;
+    } else {
+        $hdr->{name} = $hdr->{relPath};
+    }
+
+    if ( $hdr->{relPath} =~ m{(.*?)/} ) {
+        my $clientName = $1;
+        if ( $ClientName ne $clientName ) {
+            $ClientName    = $clientName;
+            $ClientBackups = [ $bpc->BackupInfoRead($ClientName) ];
+            #print STDERR "Setting Client to $ClientName\n";
+        }
+        if ( $hdr->{relPath} =~ m{(.*?)/(\d+)/}
+                 || $hdr->{relPath} =~ m{(.*?)/(\d+)$} ) {
+            my $backupNum = $2;
+            if ( $ClientBkupNum != $backupNum ) {
+                my $i;
+                $ClientBkupNum = $backupNum;
+                # print STDERR "Setting ClientBkupNum to $ClientBkupNum\n";
+                for ( $i = 0 ; $i < @$ClientBackups ; $i++ ) {
+                    if ( $ClientBackups->[$i]{num} == $ClientBkupNum ) {
+                        $ClientBkupCompress = $ClientBackups->[$i]{compress};
+                        $ClientBkupMangle   = $ClientBackups->[$i]{mangle};
+                        # print STDERR "Setting $ClientBkupNum compress to $ClientBkupCompress, mangle to $ClientBkupMangle\n";
+                        last;
+                    }
+                }
+            }
+            $hdr->{compress} = $ClientBkupCompress;
+            if ( $hdr->{type} == BPC_FTYPE_FILE && $hdr->{nlink} > 1
+                    && $hdr->{name} =~ /^f/ ) {
+                (my $dir = $hdr->{fullPath}) =~ s{(.*)/.*}{$1};
+                if ( $ClientDir ne $dir ) {
+                    $ClientDir = $dir;
+                    $ClientDirAttr = BackupPC::Attrib->new(
+                                          { compress => $ClientBkupCompress }
+                                     );
+                    if ( -f $ClientDirAttr->fileName($dir)
+                                && !$ClientDirAttr->read($dir) ) {
+                        print STDERR "Can't read attrib file in $dir\n";
+                        $ErrorCnt++;
+                    }
+                }
+                my $name = $hdr->{name};
+                $name = $bpc->fileNameUnmangle($name) if ( $ClientBkupMangle );
+                my $attr = $ClientDirAttr->get($name);
+                $hdr->{realSize} = $attr->{size} if ( defined($attr) );
+                #print STDERR "$hdr->{fullPath} has real size $hdr->{realSize}\n";
+            }
+        }
+    } else {
+        $hdr->{compress} = 0;
+        $hdr->{realSize} = $hdr->{size};
+    }
+
+    #print STDERR "$File::Find::name\n";
+
+    TarWriteFile($hdr, $fh);
+}
+
+sub UidLookup
+{
+    my($uid) = @_;
+
+    $UidCache{$uid} = (getpwuid($uid))[0] if ( !exists($UidCache{$uid}) );
+    return $UidCache{$uid};
+}
+
+sub GidLookup
+{
+    my($gid) = @_;
+
+    $GidCache{$gid} = (getgrgid($gid))[0] if ( !exists($GidCache{$gid}) );
+    return $GidCache{$gid};
+}
+
+sub TarWrite
+{
+    my($fh, $dataRef) = @_;
+
+    if ( !defined($dataRef) ) {
+        #
+        # do flush by padding to a full $WriteBufSz
+        #
+        my $data = "\0" x ($WriteBufSz - length($WriteBuf));
+        $dataRef = \$data;
+    }
+    if ( length($WriteBuf) + length($$dataRef) < $WriteBufSz ) {
+        #
+        # just buffer and return
+        #
+        $WriteBuf .= $$dataRef;
+        return;
+    }
+    my $done = $WriteBufSz - length($WriteBuf);
+    if ( (my $n = syswrite($fh, $WriteBuf . substr($$dataRef, 0, $done)))
+                                != $WriteBufSz ) {
+        print(STDERR "Unable to write to output file ($!) ($n vs $WriteBufSz)\n");
+        exit(1);
+    }
+    while ( $done + $WriteBufSz <= length($$dataRef) ) {
+        if ( (my $n = syswrite($fh, substr($$dataRef, $done, $WriteBufSz)))
+                            != $WriteBufSz ) {
+            print(STDERR "Unable to write to output file ($!) ($n v $WriteBufSz)\n");
+            exit(1);
+        }
+        $done += $WriteBufSz;
+    }
+    $WriteBuf = substr($$dataRef, $done);
+}
+
+sub TarWritePad
+{
+    my($fh, $size) = @_;
+
+    if ( $size % $tar_header_length ) {
+        my $data = "\0" x ($tar_header_length - ($size % $tar_header_length));
+        TarWrite($fh, \$data);
+    }
+}
+
+sub TarWriteHeader
+{
+    my($fh, $hdr) = @_;
+
+    $hdr->{uname} = UidLookup($hdr->{uid}) if ( !defined($hdr->{uname}) );
+    $hdr->{gname} = GidLookup($hdr->{gid}) if ( !defined($hdr->{gname}) );
+    my $devmajor = defined($hdr->{devmajor}) ? sprintf("%07o", $hdr->{devmajor})
+                                             : "";
+    my $devminor = defined($hdr->{devminor}) ? sprintf("%07o", $hdr->{devminor})
+                                             : "";
+    my $sizeStr;
+    if ( $hdr->{size} >= 2 * 65536 * 65536 ) {
+       #
+       # GNU extension for files >= 8GB: send size in big-endian binary
+       #
+       $sizeStr = pack("c4 N N", 0x80, 0, 0, 0,
+                                 $hdr->{size} / (65536 * 65536),
+                                 $hdr->{size} % (65536 * 65536));
+    } elsif ( $hdr->{size} >= 1 * 65536 * 65536 ) {
+       #
+       # sprintf octal only handles up to 2^32 - 1
+       #
+       $sizeStr = sprintf("%03o", $hdr->{size} / (1 << 24))
+                . sprintf("%08o", $hdr->{size} % (1 << 24));
+    } else {
+       $sizeStr = sprintf("%011o", $hdr->{size});
+    }
+    my $data = pack($tar_pack_header,
+                     substr($hdr->{name}, 0, 99),
+                     sprintf("%07o", $hdr->{mode}),
+                     sprintf("%07o", $hdr->{uid}),
+                     sprintf("%07o", $hdr->{gid}),
+                     $sizeStr,
+                     sprintf("%011o", $hdr->{mtime}),
+                     "",        #checksum field - space padded by pack("A8")
+                     $hdr->{type},
+                     substr($hdr->{linkname}, 0, 99),
+                     $hdr->{magic} || 'ustar ',
+                     $hdr->{version} || ' ',
+                     $hdr->{uname},
+                     $hdr->{gname},
+                     $devmajor,
+                     $devminor,
+                     ""         # prefix is empty
+                 );
+    substr($data, 148, 7) = sprintf("%06o\0", unpack("%16C*",$data));
+    TarWrite($fh, \$data);
+}
+
+sub TarWriteFileInfo
+{
+    my($fh, $hdr) = @_;
+
+    #
+    # Handle long link names (symbolic links)
+    #
+    if ( length($hdr->{linkname}) > 99 ) {
+        my %h;
+        my $data = $hdr->{linkname} . "\0";
+        $h{name} = "././\@LongLink";
+        $h{type} = "K";
+        $h{size} = length($data);
+        TarWriteHeader($fh, \%h);
+        TarWrite($fh, \$data);
+        TarWritePad($fh, length($data));
+    }
+    #
+    # Handle long file names
+    #
+    if ( length($hdr->{name}) > 99 ) {
+        my %h;
+        my $data = $hdr->{name} . "\0";
+        $h{name} = "././\@LongLink";
+        $h{type} = "L";
+        $h{size} = length($data);
+        TarWriteHeader($fh, \%h);
+        TarWrite($fh, \$data);
+        TarWritePad($fh, length($data));
+    }
+    TarWriteHeader($fh, $hdr);
+}
+
+my $Attr;
+my $AttrDir;
+
+sub TarWriteFile
+{
+    my($hdr, $fh) = @_;
+
+    my $tarPath = $hdr->{relPath};
+
+    $tarPath =~ s{//+}{/}g;
+    $tarPath = "./" . $tarPath if ( $tarPath !~ /^\.\// );
+    $tarPath =~ s{//+}{/}g;
+    $hdr->{name} = $tarPath;
+
+    if ( $hdr->{type} == BPC_FTYPE_DIR ) {
+        #
+        # Directory: just write the header
+        #
+        $hdr->{name} .= "/" if ( $hdr->{name} !~ m{/$} );
+        TarWriteFileInfo($fh, $hdr);
+       $DirCnt++;
+    } elsif ( $hdr->{type} == BPC_FTYPE_FILE ) {
+        #
+        # Regular file: write the header and file
+        #
+        my($data, $dataMD5, $size, $linkName);
+
+        if ( $hdr->{type} == BPC_FTYPE_FILE && $hdr->{nlink} > 1 ) {
+            if ( defined($Inode2Path{$hdr->{inode}}) ) {
+                $linkName = $Inode2Path{$hdr->{inode}};
+                #print STDERR "Got cache hit for $linkName\n";
+            } else {
+                my $f = BackupPC::FileZIO->open($hdr->{fullPath}, 0,
+                                                $hdr->{compress});
+                if ( !defined($f) ) {
+                    print(STDERR "Unable to open file $hdr->{fullPath}\n");
+                    $ErrorCnt++;
+                    return;
+                }
+                #
+                # Try to find the hardlink it points to by computing
+                # the pool file digest.
+                #
+                $f->read(\$dataMD5, $BufSize);
+                if ( !defined($hdr->{realSize}) ) {
+                    #
+                    # Need to get the real size
+                    #
+                    $size = length($dataMD5);
+                    while ( $f->read(\$data, $BufSize) > 0 ) {
+                        $size += length($data);
+                    }
+                    $hdr->{realSize} = $size;
+                }
+                $f->close();
+                my $md5 = Digest::MD5->new;
+                if ( $hdr->{realSize} < 1048576
+                            && length($dataMD5) != $hdr->{realSize} ) {
+                    printf(STDERR "File $hdr->{fullPath} has bad size"
+                                . " (expect $hdr->{realSize}, got %d)\n",
+                                length($dataMD5));
+                } else {
+                    my $digest = $bpc->Buffer2MD5($md5, $hdr->{realSize},
+                                                  \$dataMD5);
+                    my $path = $bpc->MD52Path($digest, $hdr->{compress});
+                    my $i = -1;
+
+                    # print(STDERR "Looking up $hdr->{fullPath} at $path\n");
+                    while ( 1 ) {
+                        my $testPath = $path;
+                        $testPath .= "_$i" if ( $i >= 0 );
+                        last if ( !-f $testPath );
+                        my $inode = (stat(_))[1];
+                        if ( $inode == $hdr->{inode} ) {
+                            #
+                            # Found it!  Just emit a tar hardlink
+                            #
+                            $testPath =~ s{\Q$TopDir\E}{..};
+                            $linkName = $testPath;
+                            last;
+                        }
+                        $i++;
+                    }
+                }
+            }
+            if ( defined($linkName) ) {
+                $hdr->{type}     = BPC_FTYPE_HARDLINK;
+                $hdr->{linkname} = $linkName;
+                TarWriteFileInfo($fh, $hdr);
+                $HLinkCnt++;
+                #print STDERR "$hdr->{relPath} matches $testPath\n";
+                if ( !$opts{c} && $hdr->{nlink} > 2 ) {
+                    #
+                    # add it to the cache if there are more
+                    # than 2 links (pool + current file),
+                    # since there are more to go
+                    #
+                    $Inode2Path{$hdr->{inode}} = $linkName;
+                }
+                return;
+            }
+            $size = 0;
+            print STDERR "Can't find $hdr->{relPath} in pool, will copy file\n";
+            $ErrorCnt++;
+        }
+
+        my $f = BackupPC::FileZIO->open($hdr->{fullPath}, 0, 0);
+        if ( !defined($f) ) {
+            print(STDERR "Unable to open file $hdr->{fullPath}\n");
+            $ErrorCnt++;
+           return;
+        }
+        TarWriteFileInfo($fh, $hdr);
+        while ( $f->read(\$data, $BufSize) > 0 ) {
+            if ( $size + length($data) > $hdr->{size} ) {
+                print(STDERR "Error: truncating $hdr->{fullPath} to"
+                           . " $hdr->{size} bytes\n");
+                $data = substr($data, 0, $hdr->{size} - $size);
+                $ErrorCnt++;
+            }
+            TarWrite($fh, \$data);
+            $size += length($data);
+        }
+        $f->close;
+        if ( $size != $hdr->{size} ) {
+            print(STDERR "Error: padding $hdr->{fullPath} to $hdr->{size}"
+                       . " bytes from $size bytes\n");
+            $ErrorCnt++;
+            while ( $size < $hdr->{size} ) {
+                my $len = $hdr->{size} - $size;
+                $len = $BufSize if ( $len > $BufSize );
+                $data = "\0" x $len;
+                TarWrite($fh, \$data);
+                $size += $len;
+            }
+        }
+        TarWritePad($fh, $size);
+       $FileCnt++;
+       $ByteCnt += $size;
+    } else {
+        print(STDERR "Got unknown type $hdr->{type} for $hdr->{name}\n");
+       $ErrorCnt++;
+    }
+}
index 7c859d2..82061af 100755 (executable)
@@ -6,15 +6,14 @@
 #
 # DESCRIPTION
 #  
-#   Usage: BackupPC_zipCreate [-t] [-h host] [-n dumpNum] [-s shareName]
-#                  [-r pathRemove] [-p pathAdd] [-c compressionLevel]
-#                   files/directories...
+#   Usage: BackupPC_zipCreate [options] files/directories...
 #
 #   Flags:
 #     Required options:
-#
 #       -h host         host from which the zip archive is created
 #       -n dumpNum      dump number from which the zip archive is created
+#                       A negative number means relative to the end (eg -1
+#                       means the most recent dump, -2 2nd most recent etc).
 #       -s shareName    share name from which the zip archive is created
 #
 #     Other options:
@@ -22,6 +21,8 @@
 #       -r pathRemove   path prefix that will be replaced with pathAdd
 #       -p pathAdd      new path prefix
 #       -c level        compression level (default is 0, no compression)
+#       -e charset      charset for encoding file names (default: value of
+#                       $Conf{ClientCharset} when backup was done)
 #
 #     The -h, -n and -s options specify which dump is used to generate
 #     the zip archive.  The -r and -p options can be used to relocate
@@ -63,6 +64,7 @@ use lib "/usr/local/BackupPC/lib";
 use Archive::Zip qw(:ERROR_CODES);
 use File::Path;
 use Getopt::Std;
+use Encode qw/from_to/;
 use IO::Handle;
 use BackupPC::Lib;
 use BackupPC::Attrib qw(:all);
@@ -77,10 +79,24 @@ my %Conf   = $bpc->Conf();
 
 my %opts;
 
-if ( !getopts("th:n:p:r:s:c:", \%opts) || @ARGV < 1 ) {
-    print(STDERR "usage: $0 [-t] [-h host] [-n dumpNum] [-s shareName]"
-               . " [-r pathRemove] [-p pathAdd] [-c compressionLevel]"
-               . " files/directories...\n");
+if ( !getopts("te:h:n:p:r:s:c:", \%opts) || @ARGV < 1 ) {
+    print STDERR <<EOF;
+usage: $0 [options] files/directories...
+  Required options:
+     -h host         host from which the zip archive is created
+     -n dumpNum      dump number from which the tar archive is created
+                     A negative number means relative to the end (eg -1
+                     means the most recent dump, -2 2nd most recent etc).
+     -s shareName    share name from which the zip archive is created
+
+  Other options:
+     -t              print summary totals
+     -r pathRemove   path prefix that will be replaced with pathAdd
+     -p pathAdd      new path prefix
+     -c level        compression level (default is 0, no compression)
+     -e charset      charset for encoding file names (default: value of
+                     \$Conf{ClientCharset} when backup was done)
+EOF
     exit(1);
 }
 
@@ -90,11 +106,12 @@ if ( $opts{h} !~ /^([\w\.\s-]+)$/ ) {
 }
 my $Host = $opts{h};
 
-if ( $opts{n} !~ /^(\d+)$/ ) {
+if ( $opts{n} !~ /^(-?\d+)$/ ) {
     print(STDERR "$0: bad dump number '$opts{n}'\n");
     exit(1);
 }
 my $Num = $opts{n};
+
 $opts{c} = 0 if ( $opts{c} eq "" );
 if ( $opts{c} !~ /^(\d+)$/ ) {
     print(STDERR "$0: invalid compression level '$opts{c}'. 0=none, 9=max\n");
@@ -103,13 +120,14 @@ if ( $opts{c} !~ /^(\d+)$/ ) {
 my $compLevel = $opts{c};
 
 my @Backups = $bpc->BackupInfoRead($Host);
-my($i);
 my $FileCnt = 0;
 my $ByteCnt = 0;
 my $DirCnt = 0;
 my $SpecialCnt = 0;
 my $ErrorCnt = 0;
 
+my $i;
+$Num = $Backups[@Backups + $Num]{num} if ( -@Backups <= $Num && $Num < 0 );
 for ( $i = 0 ; $i < @Backups ; $i++ ) {
     last if ( $Backups[$i]{num} == $Num );
 }
@@ -118,6 +136,9 @@ if ( $i >= @Backups ) {
     exit(1);
 }
 
+my $Charset = $Backups[$i]{charset};
+$Charset = $opts{e} if ( $opts{e} ne "" );
+
 my $PathRemove = $1 if ( $opts{r} =~ /(.+)/ );
 my $PathAdd    = $1 if ( $opts{p} =~ /(.+)/ );
 if ( $opts{s} !~ /^([\w\s.\/$(){}[\]-]+)$/ ) {
@@ -200,9 +221,10 @@ sub ZipWriteFile
             && substr($tarPath, 0, length($PathRemove)) eq $PathRemove ) {
         substr($tarPath, 0, length($PathRemove)) = $PathAdd;
     }
-    $tarPath = "./" . $tarPath if ( $tarPath !~ /^\.\// );
+    $tarPath = $1 if ( $tarPath =~ m{^\.?/+(.*)} );
     $tarPath =~ s{//+}{/}g;
     $hdr->{name} = $tarPath;
+    return if ( $tarPath eq "." || $tarPath eq "./" || $tarPath eq "" );
 
     my $zipmember; # Container to hold the file/directory to zip.
 
@@ -211,12 +233,14 @@ sub ZipWriteFile
         # Directory: just write the header
         #
         $hdr->{name} .= "/" if ( $hdr->{name} !~ m{/$} );
+        from_to($hdr->{name}, "utf8", $Charset) if ( $Charset ne "" );
         $zipmember = Archive::Zip::Member->newDirectoryNamed($hdr->{name});
        $DirCnt++;
     } elsif ( $hdr->{type} == BPC_FTYPE_FILE ) {
         #
         # Regular file: write the header and file
         #
+        from_to($hdr->{name}, "utf8", $Charset) if ( $Charset ne "" );
         $zipmember = BackupPC::Zip::FileMember->newFromFileNamed(
                                            $hdr->{fullPath},
                                            $hdr->{name},
index 9beca1a..19e77e3 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * BackupPC standard CSS definitions
  *
- * Version 2.1.0, released 13 Jun 2004.
+ * Version 2.1.0, released 20 Jun 2004.
  *
  * See http://backuppc.sourceforge.net.
  *
@@ -92,6 +92,50 @@ a.navbar {
     font-size:10pt;
 }
 
+.editHeader {
+    font-family:arial,sans-serif;
+    font-size:12pt;
+    color:#000000;
+    font-weight:bold;
+    background-color:#99cc33;
+}
+
+.editTabSel {
+    font-family:arial,sans-serif;
+    font-size:14pt;
+    color:#000000;
+    font-weight:bold;
+    background-color:#99cc33;
+    padding:3px;
+    padding-left:6px;
+    margin-bottom:5px;
+}
+
+.editTabNoSel {
+    font-family:arial,sans-serif;
+    font-size:14pt;
+    color:#000000;
+    font-weight:bold;
+    padding:3px;
+    padding-left:6px;
+    margin-bottom:5px;
+}
+
+.editSaveButton {
+    color:#ff0000;
+    font-size:14pt;
+    font-weight:bold;
+}
+
+.editError {
+    color:#ff0000;
+    font-weight:bold;
+}
+
+.editComment {
+    font-size:10pt;
+}
+
 .fviewheader {
     font-weight:bold;
     font-size:10pt;
index 1284c5d..03c28b5 100644 (file)
@@ -547,6 +547,9 @@ $Conf{ArchiveInfoKeepCnt} = 10;
 # array, and $Conf{SmbShareName} contains multiple share names, then
 # the setting is assumed to apply all shares.
 #
+# If a hash is used, a special key "*" means it applies to all
+# shares.
+#
 # Examples:
 #    $Conf{BackupFilesOnly} = '/myFiles';
 #    $Conf{BackupFilesOnly} = ['/myFiles'];     # same as first example
@@ -555,6 +558,10 @@ $Conf{ArchiveInfoKeepCnt} = 10;
 #       'c' => ['/myFiles', '/important'],      # these are for 'c' share
 #       'd' => ['/moreFiles', '/archive'],      # these are for 'd' share
 #    };
+#    $Conf{BackupFilesOnly} = {
+#       'c' => ['/myFiles', '/important'],      # these are for 'c' share
+#       '*' => ['/myFiles', '/important'],      # these are other shares
+#    };
 #
 $Conf{BackupFilesOnly} = undef;
 
@@ -590,6 +597,9 @@ $Conf{BackupFilesOnly} = undef;
 # Users report that for smbclient you should specify a directory
 # followed by "/*", eg: "/proc/*", instead of just "/proc".
 #
+# If a hash is used, a special key "*" means it applies to all
+# shares.
+#
 # Examples:
 #    $Conf{BackupFilesExclude} = '/temp';
 #    $Conf{BackupFilesExclude} = ['/temp'];     # same as first example
@@ -598,6 +608,10 @@ $Conf{BackupFilesOnly} = undef;
 #       'c' => ['/temp', '/winnt/tmp'],         # these are for 'c' share
 #       'd' => ['/junk', '/dont_back_this_up'], # these are for 'd' share
 #    };
+#    $Conf{BackupFilesExclude} = {
+#       'c' => ['/temp', '/winnt/tmp'],         # these are for 'c' share
+#       '*' => ['/junk', '/dont_back_this_up'], # these are for other shares
+#    };
 #
 $Conf{BackupFilesExclude} = undef;
 
@@ -1759,6 +1773,11 @@ $Conf{CgiImageDirURL} = '';
 #
 $Conf{CgiCSSFile} = 'BackupPC_stnd.css';
 
+#
+# Whether the user is allowed to edit their per-PC config.
+#
+$Conf{CgiUserConfigEditEnable} = 1;
+
 #
 # Which per-host config variables a non-admin user is allowed
 # to edit.
index ec63ebb..0b8e030 100755 (executable)
@@ -78,21 +78,28 @@ EOF
 }
 
 my %opts;
+$opts{fhs} = 1;
+$opts{"set-perms"} = 1;
+$opts{"backuppc-user"} = "backuppc";
 if ( !GetOptions(
             \%opts,
             "batch",
+            "backuppc-user=s",
             "bin-path=s%",
-            "config-path=s",
             "cgi-dir=s",
+            "compress-level=i",
+            "config-path=s",
             "data-dir=s",
             "dest-dir=s",
+            "fhs!",
             "help|?",
             "hostname=s",
             "html-dir=s",
             "html-dir-url=s",
             "install-dir=s",
             "man",
-            "uid-ignore",
+            "set-perms!",
+            "uid-ignore!",
         ) || @ARGV ) {
     pod2usage(2);
 }
@@ -101,7 +108,7 @@ pod2usage(-exitstatus => 0, -verbose => 2) if $opts{man};
 
 my $DestDir = $opts{"dest-dir"};
 
-if ( $< != 0 ) {
+if ( !$opts{"uid-ignore"} && $< != 0 ) {
     print <<EOF;
 
 This configure script should be run as root, rather than uid $<.
@@ -115,11 +122,29 @@ EOF
     exit(1) if ( $opts{batch} && !$opts{"uid-ignore"} );
 }
 
-print <<EOF;
+#
+# Whether we use the file system hierarchy conventions or not.
+# Older versions did not.  BackupPC used to be installed in
+# two main directories (in addition to CGI and html pages)
+#
+#    TopDir       which includes subdirs conf, log, pc, pool, cpool
+#                
+#    InstallDir   which includes subdirs bin, lib, doc
+#
+# With FSH enabled (which is the default for new installations):
+#
+#    /etc/BackupPC/config.pl  main config file (was $TopDir/conf/config.pl)
+#    /etc/BackupPC/hosts      hosts file (was $TopDir/conf/hosts)
+#    /etc/BackupPC/pc/HOST.pl per-pc config file (was $TopDir/pc/HOST/config.pl)
+#    /var/log/BackupPC        log files (was $TopDir/log)
+#    /var/lib/BackupPC        Pid, status and email info (was $TopDir/log)
+#
+
+print <<EOF if ( !$opts{fhs} || !-f "/etc/BackupPC/config.pl" );
 
 Is this a new installation or upgrade for BackupPC?  If this is
 an upgrade please tell me the full path of the existing BackupPC
-configuration file (eg: /xxxx/conf/config.pl).  Otherwise, just
+configuration file (eg: /etc/BackupPC/config.pl).  Otherwise, just
 hit return.
 
 EOF
@@ -130,29 +155,38 @@ EOF
 #
 my $ConfigPath = "";
 while ( 1 ) {
-    $ConfigPath = prompt("--> Full path to existing conf/config.pl",
-                         $ConfigPath,
-                         "config-path");
+    if ( $opts{fhs} && -f "/etc/BackupPC/config.pl" ) {
+        $ConfigPath = "/etc/BackupPC/config.pl";
+    } else {
+        $ConfigPath = prompt("--> Full path to existing main config.pl",
+                             $ConfigPath,
+                             "config-path");
+    }
     last if ( $ConfigPath eq ""
             || ($ConfigPath =~ /^\// && -r $ConfigPath && -w $ConfigPath) );
     my $problem = "is not an absolute path";
-    $problem = "is not writable" if ( !-w $ConfigPath );
-    $problem = "is not readable" if ( !-r $ConfigPath );
-    $problem = "doesn't exist"   if ( !-f $ConfigPath );
+    $problem = "is not writable"        if ( !-w $ConfigPath );
+    $problem = "is not readable"        if ( !-r $ConfigPath );
+    $problem = "is not a regular file"  if ( !-f $ConfigPath );
+    $problem = "doesn't exist"          if ( !-e $ConfigPath );
     print("The file '$ConfigPath' $problem.\n");
     if ( $opts{batch} ) {
         print("Need to specify a valid --config-path for upgrade\n");
         exit(1);
     }
 }
+
 my $bpc;
 if ( $ConfigPath ne "" && -r $ConfigPath ) {
-    (my $topDir = $ConfigPath) =~ s{/[^/]+/[^/]+$}{};
+    (my $confDir = $ConfigPath) =~ s{/[^/]+$}{};
     die("BackupPC::Lib->new failed\n")
-            if ( !($bpc = BackupPC::Lib->new($topDir, ".", 1)) );
+            if ( !($bpc = BackupPC::Lib->new(".", ".", $confDir, 1)) );
     %Conf = $bpc->Conf();
     %OrigConf = %Conf;
-    $Conf{TopDir} = $topDir;
+    if ( !$opts{fhs} ) {
+        ($Conf{TopDir} = $ConfigPath) =~ s{/[^/]+/[^/]+$}{};
+    }
+    $Conf{ConfDir} = $confDir;
     my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort}, 1);
     if ( $err eq "" ) {
         print <<EOF;
@@ -166,6 +200,17 @@ EOF
     }
 }
 
+#
+# Create defaults for FHS setup
+#
+if ( $opts{fhs} ) {
+    $Conf{TopDir}       ||= "/data/BackupPC";
+    $Conf{ConfDir}      ||= "/etc/BackupPC";
+    $Conf{InstallDir}   ||= "/usr/local/BackupPC";
+    $Conf{LogDir}       ||= "/var/log/BackupPC";
+    $Conf{StatusDir}    ||= "/var/lib/BackupPC";
+}
+
 #
 # These are the programs whose paths we need to find
 #
@@ -255,16 +300,18 @@ my($name, $passwd, $Uid, $Gid);
 while ( 1 ) {
     $Conf{BackupPCUser} = prompt("--> BackupPC should run as user",
                                  $Conf{BackupPCUser} || "backuppc",
-                                 "username");
-    ($name, $passwd, $Uid, $Gid) = getpwnam($Conf{BackupPCUser});
-    last if ( $name ne "" );
-    print <<EOF;
+                                 "backuppc-user");
+    if ( $opts{"set-perms"} ) {
+        ($name, $passwd, $Uid, $Gid) = getpwnam($Conf{BackupPCUser});
+        last if ( $name ne "" );
+        print <<EOF;
 
-getpwnam() says that user $Conf{BackupPCUser} doesn't exist.  Please check the
-name and verify that this user is in the passwd file.
+getpwnam() says that user $Conf{BackupPCUser} doesn't exist.  Please
+check the name and verify that this user is in the passwd file.
 
 EOF
-    exit(1) if ( $opts{batch} );
+        exit(1) if ( $opts{batch} );
+    }
 }
 
 print <<EOF;
@@ -288,9 +335,9 @@ while ( 1 ) {
 print <<EOF;
 
 Please specify a data directory for BackupPC.  This is where the
-configuration files, LOG files and all the PC backups are stored.
-This file system needs to be big enough to accommodate all the
-PCs you expect to backup (eg: at least 1-2GB per machine).
+all the PC backup data is stored.  This file system needs to be
+big enough to accommodate all the PCs you expect to backup (eg:
+at least several GB per machine).
 
 EOF
 
@@ -305,6 +352,9 @@ while ( 1 ) {
     }
 }
 
+$Conf{CompressLevel} = $opts{"compress-level"}
+                            if ( defined($opts{"compress-level"}) );
+
 if ( !defined($Conf{CompressLevel}) ) {
     $Conf{CompressLevel} = BackupPC::FileZIO->compOk ? 3 : 0;
     if ( $ConfigPath eq "" && $Conf{CompressLevel} ) {
@@ -435,7 +485,7 @@ Ok, we're about to:
 
   - install the binaries, lib and docs in $Conf{InstallDir},
   - create the data directory $Conf{TopDir},
-  - create/update the config.pl file $Conf{TopDir}/conf,
+  - create/update the config.pl file $Conf{ConfDir}/config.pl,
   - optionally install the cgi-bin interface.
 
 EOF
@@ -447,16 +497,14 @@ exit unless prompt("--> Do you want to continue?", "y") =~ /y/i;
 #
 foreach my $dir ( qw(bin doc
                     lib/BackupPC/CGI
-                    lib/BackupPC/Config
                     lib/BackupPC/Lang
-                    lib/BackupPC/Storage
                     lib/BackupPC/Xfer
                     lib/BackupPC/Zip
                 ) ) {
     next if ( -d "$DestDir$Conf{InstallDir}/$dir" );
     mkpath("$DestDir$Conf{InstallDir}/$dir", 0, 0775);
     if ( !-d "$DestDir$Conf{InstallDir}/$dir"
-            || !chown($Uid, $Gid, "$DestDir$Conf{InstallDir}/$dir") ) {
+            || !my_chown($Uid, $Gid, "$DestDir$Conf{InstallDir}/$dir") ) {
         die("Failed to create or chown $DestDir$Conf{InstallDir}/$dir\n");
     } else {
         print("Created $DestDir$Conf{InstallDir}/$dir\n");
@@ -467,9 +515,9 @@ foreach my $dir ( qw(bin doc
 # Create CGI image directory
 #
 foreach my $dir ( ($Conf{CgiImageDir}) ) {
-    next if ( $dir eq "" || -d "$DestDir$dir" );
+    next if ( $dir eq "" || -d $dir );
     mkpath("$DestDir$dir", 0, 0775);
-    if ( !-d "$DestDir$dir" || !chown($Uid, $Gid, "$DestDir$dir") ) {
+    if ( !-d "$DestDir$dir" || !my_chown($Uid, $Gid, "$DestDir$dir") ) {
         die("Failed to create or chown $DestDir$dir");
     } else {
         print("Created $DestDir$dir\n");
@@ -477,15 +525,24 @@ foreach my $dir ( ($Conf{CgiImageDir}) ) {
 }
 
 #
-# Create $TopDir's top-level directories
+# Create other directories
 #
-foreach my $dir ( qw(. conf pool cpool pc trash log) ) {
-    mkpath("$DestDir$Conf{TopDir}/$dir", 0, 0750) if ( !-d "$DestDir$Conf{TopDir}/$dir" );
-    if ( !-d "$DestDir$Conf{TopDir}/$dir"
-            || !chown($Uid, $Gid, "$DestDir$Conf{TopDir}/$dir") ) {
-        die("Failed to create or chown $DestDir$Conf{TopDir}/$dir\n");
+foreach my $dir ( (
+            "$Conf{TopDir}",
+            "$Conf{TopDir}/pool",
+            "$Conf{TopDir}/cpool",
+            "$Conf{TopDir}/pc",
+            "$Conf{TopDir}/trash",
+            "$Conf{ConfDir}",
+            "$Conf{LogDir}",
+            "$Conf{StatusDir}",
+        ) ) {
+    mkpath("$DestDir/$dir", 0, 0750) if ( !-d "$DestDir/$dir" );
+    if ( !-d "$DestDir/$dir"
+            || !my_chown($Uid, $Gid, "$DestDir/$dir") ) {
+        die("Failed to create or chown $DestDir/$dir\n");
     } else {
-        print("Created $DestDir$Conf{TopDir}/$dir\n");
+        print("Created $DestDir/$dir\n");
     }
 }
 
@@ -501,12 +558,23 @@ foreach my $prog ( qw(BackupPC BackupPC_dump BackupPC_link BackupPC_nightly
 printf("Installing library in $DestDir$Conf{InstallDir}/lib\n");
 foreach my $lib ( qw(
        BackupPC/Lib.pm
-       BackupPC/Attrib.pm
        BackupPC/FileZIO.pm
-        BackupPC/Config.pm
+       BackupPC/Attrib.pm
         BackupPC/PoolWrite.pm
-        BackupPC/Storage.pm
        BackupPC/View.pm
+       BackupPC/Xfer/Archive.pm
+       BackupPC/Xfer/Tar.pm
+        BackupPC/Xfer/Smb.pm
+       BackupPC/Xfer/Rsync.pm
+       BackupPC/Xfer/RsyncDigest.pm
+        BackupPC/Xfer/RsyncFileIO.pm
+       BackupPC/Zip/FileMember.pm
+        BackupPC/Lang/en.pm
+       BackupPC/Lang/fr.pm
+       BackupPC/Lang/es.pm
+        BackupPC/Lang/de.pm
+        BackupPC/Lang/it.pm
+        BackupPC/Lang/nl.pm
         BackupPC/CGI/AdminOptions.pm
        BackupPC/CGI/Archive.pm
        BackupPC/CGI/ArchiveInfo.pm
@@ -527,22 +595,6 @@ foreach my $lib ( qw(
         BackupPC/CGI/StopServer.pm
        BackupPC/CGI/Summary.pm
        BackupPC/CGI/View.pm
-       BackupPC/Config/Meta.pm
-        BackupPC/Lang/en.pm
-       BackupPC/Lang/fr.pm
-       BackupPC/Lang/es.pm
-        BackupPC/Lang/de.pm
-        BackupPC/Lang/it.pm
-        BackupPC/Lang/nl.pm
-       BackupPC/Lang/pt_br.pm
-       BackupPC/Storage/Text.pm
-       BackupPC/Xfer/Archive.pm
-       BackupPC/Xfer/Tar.pm
-        BackupPC/Xfer/Smb.pm
-       BackupPC/Xfer/Rsync.pm
-       BackupPC/Xfer/RsyncDigest.pm
-        BackupPC/Xfer/RsyncFileIO.pm
-       BackupPC/Zip/FileMember.pm
     ) ) {
     InstallFile("lib/$lib", "$DestDir$Conf{InstallDir}/lib/$lib", 0444);
 }
@@ -577,16 +629,16 @@ foreach my $doc ( qw(BackupPC.pod BackupPC.html) ) {
     InstallFile("doc/$doc", "$DestDir$Conf{InstallDir}/doc/$doc", 0444);
 }
 
-printf("Installing config.pl and hosts in $DestDir$Conf{TopDir}/conf\n");
-InstallFile("conf/hosts", "$DestDir$Conf{TopDir}/conf/hosts", 0644)
-                    if ( !-f "$DestDir$Conf{TopDir}/conf/hosts" );
+printf("Installing config.pl and hosts in $DestDir$Conf{ConfDir}\n");
+InstallFile("conf/hosts", "$DestDir$Conf{ConfDir}/hosts", 0644)
+                    if ( !-f "$DestDir$Conf{ConfDir}/hosts" );
 
 #
 # Now do the config file.  If there is an existing config file we
 # merge in the new config file, adding any new configuration
 # parameters and deleting ones that are no longer needed.
 #
-my $dest = "$DestDir$Conf{TopDir}/conf/config.pl";
+my $dest = "$DestDir$Conf{ConfDir}/config.pl";
 my ($newConf, $newVars) = ConfigParse("conf/config.pl");
 my ($oldConf, $oldVars);
 if ( -f $dest ) {
@@ -711,10 +763,12 @@ if ( -f $dest && !-f $confCopy ) {
     my $mode = $stat[2];
     my $uid  = $stat[4];
     my $gid  = $stat[5];
-    die("can't copy($dest, $confCopy)\n")  unless copy($dest, $confCopy);
+    die("can't copy($dest, $confCopy)\n")
+                                unless copy($dest, $confCopy);
     die("can't chown $uid, $gid $confCopy\n")
-                                           unless chown($uid, $gid, $confCopy);
-    die("can't chmod $mode $confCopy\n")   unless chmod($mode, $confCopy);
+                                unless my_chown($uid, $gid, $confCopy);
+    die("can't chmod $mode $confCopy\n")
+                                unless my_chmod($mode, $confCopy);
 }
 open(OUT, ">", $dest) || die("can't open $dest for writing\n");
 binmode(OUT);
@@ -735,8 +789,8 @@ foreach my $var ( @$newConf ) {
 }
 close(OUT);
 if ( !defined($oldConf) ) {
-    die("can't chmod 0640 mode $dest\n")  unless chmod(0640, $dest);
-    die("can't chown $Uid, $Gid $dest\n") unless chown($Uid, $Gid, $dest);
+    die("can't chmod 0640 mode $dest\n")  unless my_chmod(0640, $dest);
+    die("can't chown $Uid, $Gid $dest\n") unless my_chown($Uid, $Gid, $dest);
 }
 
 if ( $Conf{CgiDir} ne "" ) {
@@ -751,12 +805,12 @@ print <<EOF;
 Ok, it looks like we are finished.  There are several more things you
 will need to do:
 
-  - Browse through the config file, $Conf{TopDir}/conf/config.pl,
+  - Browse through the config file, $Conf{ConfDir}/config.pl,
     and make sure all the settings are correct.  In particular, you
     will need to set the smb share password and user name, backup
     policies and check the email message headers and bodies.
 
-  - Edit the list of hosts to backup in $Conf{TopDir}/conf/hosts.
+  - Edit the list of hosts to backup in $Conf{ConfDir}/hosts.
 
   - Read the documentation in $Conf{InstallDir}/doc/BackupPC.html.
     Please pay special attention to the security section.
@@ -789,9 +843,9 @@ EOF
 }
 
 eval "use File::RsyncP;";
-if ( !$@ && $File::RsyncP::VERSION < 0.51 ) {
+if ( !$@ && $File::RsyncP::VERSION < 0.52 ) {
     print("\nWarning: you need to upgrade File::RsyncP;"
-        . " I found $File::RsyncP::VERSION and BackupPC needs 0.51\n");
+        . " I found $File::RsyncP::VERSION and BackupPC needs 0.52\n");
 }
 
 exit(0);
@@ -825,6 +879,8 @@ sub InstallFile
        binmode(OUT);
        while ( <PROG> ) {
            s/__INSTALLDIR__/$Conf{InstallDir}/g;
+           s/__LOGDIR__/$Conf{LogDir}/g;
+           s/__CONFDIR__/$Conf{ConfDir}/g;
            s/__TOPDIR__/$Conf{TopDir}/g;
            s/__BACKUPPCUSER__/$Conf{BackupPCUser}/g;
            s/__CGIDIR__/$Conf{CgiDir}/g;
@@ -841,8 +897,8 @@ sub InstallFile
        close(PROG);
        close(OUT);
     }
-    die("can't chown $uid, $gid $dest") unless chown($uid, $gid, $dest);
-    die("can't chmod $mode $dest")      unless chmod($mode, $dest);
+    die("can't chown $uid, $gid $dest") unless my_chown($uid, $gid, $dest);
+    die("can't chmod $mode $dest")      unless my_chmod($mode, $dest);
 }
 
 sub FindProgram
@@ -954,6 +1010,22 @@ sub ConfigMerge
     return $res;
 }
 
+sub my_chown
+{
+    my($uid, $gid, $file) = @_;
+
+    return 1 if ( !$opts{"set-perms"} );
+    return chown($uid, $gid, $file);
+}
+
+sub my_chmod
+{
+    my ($mode, $file) = @_;
+
+    return 1 if ( !$opts{"set-perms"} );
+    return chmod($mode, $file);
+}
+
 sub prompt
 {
     my($question, $default, $option) = @_;
@@ -998,6 +1070,11 @@ Run configure.pl in batch mode.  configure.pl will run without
 prompting the user.  The other command-line options are used
 to specify the settings that the user is usually prompted for.
 
+=item B<--backuppc-user=USER>
+
+Specify the BackupPC user name that owns all the BackupPC
+files and runs the BackupPC programs.  Default is backuppc.
+
 =item B<--bin-path PROG=PATH>
 
 Specify the path for various external programs that BackupPC
@@ -1015,6 +1092,11 @@ Examples
 
     --bin-path cat=/bin/cat --bin-path bzip2=/home/user/bzip2
 
+=item B<--compress-level=N>
+
+Set the configuration compression level to N.  Default is 3
+if Compress::Zlib is installed.
+
 =item B<--config-path CONFIG_PATH>
 
 Path to the existing config.pl configuration file for BackupPC.
@@ -1050,6 +1132,12 @@ to run it from below the --dest-dir directory, since all the
 paths are set assuming BackupPC is installed in the intended
 final locations.
 
+=item B<--fhs>
+
+Use locations specified by the Filesystem Hierarchy Standard
+for installing BackupPC.  This is enabled by default.  To
+use the pre-3.0 installation locations, specify --no-fhs.
+
 =item B<--help|?>
 
 Print a brief help message and exits.
@@ -1094,6 +1182,13 @@ Example:
 
 Prints the manual page and exits.
 
+=item B<--set-perms>
+
+When installing files and creating directories, chown them to
+the BackupPC user and chmod them too.  This is enabled by default.
+To disable (for example, if staging a destination directory)
+then specify --no-set-perms.
+
 =item B<--uid-ignore>
 
 configure.pl verifies that the script is being run as the super user
index dc831c2..6633f59 100644 (file)
@@ -44,31 +44,31 @@ use Data::Dumper;
 
 our %ConfigMenu = (
     server => {
-        text  => "Server",
+        text  => "CfgEdit_Title_Server",
         param => [
-            {text => "General Parameters"},
+            {text => "CfgEdit_Title_General_Parameters"},
             {name => "ServerHost"},
             {name => "BackupPCUser"},
             {name => "BackupPCUserVerify"},
             {name => "MaxOldLogFiles"},
             {name => "TrashCleanSleepSec"},
 
-            {text => "Wakeup Schedule"},
+            {text => "CfgEdit_Title_Wakeup_Schedule"},
             {name => "WakeupSchedule"},
 
-            {text => "Concurrent Jobs"},
+            {text => "CfgEdit_Title_Concurrent_Jobs"},
             {name => "MaxBackups"},
             {name => "MaxUserBackups"},
             {name => "MaxPendingCmds"},
             {name => "MaxBackupPCNightlyJobs"},
             {name => "BackupPCNightlyPeriod"},
 
-            {text => "Pool Filesystem Limits"},
+            {text => "CfgEdit_Title_Pool_Filesystem_Limits"},
            {name => "DfCmd"},
            {name => "DfMaxUsagePct"},
            {name => "HardLinkMax"},
 
-            {text => "Other Parameters"},
+            {text => "CfgEdit_Title_Other_Parameters"},
            {name => "UmaskMode"},
            {name => "MyPath"},
             {name => "DHCPAddressRanges"},
@@ -76,11 +76,11 @@ our %ConfigMenu = (
             {name => "ServerInitdPath"},
             {name => "ServerInitdStartCmd"},
 
-            {text => "Remote Apache Settings"},
+            {text => "CfgEdit_Title_Remote_Apache_Settings"},
             {name => "ServerPort"},
             {name => "ServerMesgSecret"},
 
-            {text => "Program Paths"},
+            {text => "CfgEdit_Title_Program_Paths"},
            {name => "SshPath"},
            {name => "NmbLookupPath"},
            {name => "PingPath"},
@@ -91,22 +91,25 @@ our %ConfigMenu = (
            {name => "GzipPath"},
            {name => "Bzip2Path"},
 
-            {text => "Install Paths"},
+            {text => "CfgEdit_Title_Install_Paths"},
+            {name => "TopDir"},
+            {name => "ConfDir"},
+            {name => "LogDir"},
            {name => "CgiDir"},
            {name => "InstallDir"},
         ],
     },
     email => {
-        text  => "Email",
+        text  => "CfgEdit_Title_Email",
         param => [
-            {text => "Email settings"},
+            {text => "CfgEdit_Title_Email_settings"},
             {name => "SendmailPath"},
             {name => "EMailNotifyMinDays"},
             {name => "EMailFromUserName"},
             {name => "EMailAdminUserName"},
             {name => "EMailUserDestDomain"},
 
-            {text => "Email User Messages"},
+            {text => "CfgEdit_Title_Email_User_Messages"},
            {name => "EMailNoBackupEverSubj"},
            {name => "EMailNoBackupEverMesg"},
            {name => "EMailNotifyOldBackupDays"},
@@ -115,19 +118,17 @@ our %ConfigMenu = (
            {name => "EMailNotifyOldOutlookDays"},
            {name => "EMailOutlookBackupSubj"},
            {name => "EMailOutlookBackupMesg"},
+           {name => "EMailHeaders"},
         ],
     },
     cgi => {
-        text => "CGI",
+        text => "CfgEdit_Title_CGI",
         param => [
-           {text => "Admin Privileges"},
+           {text => "CfgEdit_Title_Admin_Privileges"},
            {name => "CgiAdminUserGroup"},
            {name => "CgiAdminUsers"},
 
-           {text => "Config Editing"},
-           {name => "CgiUserConfigEdit"},
-
-           {text => "Page Rendering"},
+           {text => "CfgEdit_Title_Page_Rendering"},
            {name => "Language"},
            {name => "CgiNavBarAdminAllHosts"},
            {name => "CgiSearchBoxEnable"},
@@ -138,25 +139,29 @@ our %ConfigMenu = (
            {name => "CgiExt2ContentType"},
            {name => "CgiCSSFile"},
 
-           {text => "Paths"},
+           {text => "CfgEdit_Title_Paths"},
            {name => "CgiURL"},
            {name => "CgiImageDir"},
            {name => "CgiImageDirURL"},
 
-           {text => "User URLs"},
+           {text => "CfgEdit_Title_User_URLs"},
            {name => "CgiUserHomePageCheck"},
            {name => "CgiUserUrlCreate"},
 
+           {text => "CfgEdit_Title_User_Config_Editing"},
+           {name => "CgiUserConfigEditEnable"},
+           {name => "CgiUserConfigEdit"},
         ],
     },
     xfer => {
-        text => "Xfer",
+        text => "CfgEdit_Title_Xfer",
         param => [
-            {text => "Xfer Settings"},
+            {text => "CfgEdit_Title_Xfer_Settings"},
             {name => "XferMethod", onchangeSubmit => 1},
             {name => "XferLogLevel"},
+            {name => "ClientCharset"},
 
-            {text => "Smb Settings",
+            {text => "CfgEdit_Title_Smb_Settings",
                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
             {name => "SmbShareName",
                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
@@ -165,14 +170,14 @@ our %ConfigMenu = (
             {name => "SmbSharePasswd",
                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
 
-            {text => "Tar Settings",
+            {text => "CfgEdit_Title_Tar_Settings",
                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
             {name => "TarShareName",
                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
 
-            {text => "Rsync Settings",
+            {text => "CfgEdit_Title_Rsync_Settings",
                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
-            {text => "Rsyncd Settings",
+            {text => "CfgEdit_Title_Rsyncd_Settings",
                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
             {name => "RsyncShareName",
                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
@@ -183,7 +188,7 @@ our %ConfigMenu = (
             {name => "RsyncCsumCacheVerifyProb",
                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
 
-            {text => "Archive Settings",
+            {text => "CfgEdit_Title_Archive_Settings",
                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
             {name => "ArchiveDest",
                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
@@ -194,14 +199,14 @@ our %ConfigMenu = (
             {name => "ArchiveSplit",
                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
 
-            {text => "Include/Exclude",
+            {text => "CfgEdit_Title_Include_Exclude",
                 visible => sub { return $_[0]->{XferMethod} ne "archive"; } },
             {name => "BackupFilesOnly",
                 visible => sub { return $_[0]->{XferMethod} ne "archive"; } },
             {name => "BackupFilesExclude",
                 visible => sub { return $_[0]->{XferMethod} ne "archive"; } },
 
-            {text => "Smb Paths/Commands",
+            {text => "CfgEdit_Title_Smb_Paths_Commands",
                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
             {name => "SmbClientPath",
                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
@@ -212,7 +217,7 @@ our %ConfigMenu = (
             {name => "SmbClientRestoreCmd",
                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
 
-            {text => "Tar Paths/Commands",
+            {text => "CfgEdit_Title_Tar_Paths_Commands",
                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
             {name => "TarClientPath",
                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
@@ -225,9 +230,9 @@ our %ConfigMenu = (
             {name => "TarClientRestoreCmd",
                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
 
-            {text => "Rsync Paths/Commands/Args",
+            {text => "CfgEdit_Title_Rsync_Paths_Commands_Args",
                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
-            {text => "Rsyncd Port/Args",
+            {text => "CfgEdit_Title_Rsyncd_Port_Args",
                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
             {name => "RsyncClientPath",
                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
@@ -242,7 +247,7 @@ our %ConfigMenu = (
             {name => "RsyncRestoreArgs",
                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
 
-            {text => "Archive Paths/Commands",
+            {text => "CfgEdit_Title_Archive_Paths_Commands",
                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
             {name => "ArchiveClientCmd",
                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
@@ -250,27 +255,27 @@ our %ConfigMenu = (
         ],
     },
     schedule => {
-        text => "Schedule",
+        text => "CfgEdit_Title_Schedule",
         param => [
-           {text => "Full Backups"},
+           {text => "CfgEdit_Title_Full_Backups"},
            {name => "FullPeriod"},
            {name => "FullKeepCnt"},
            {name => "FullKeepCntMin"},
            {name => "FullAgeMax"},
 
-           {text => "Incremental Backups"},
+           {text => "CfgEdit_Title_Incremental_Backups"},
            {name => "IncrPeriod"},
            {name => "IncrKeepCnt"},
            {name => "IncrKeepCntMin"},
            {name => "IncrAgeMax"},
            {name => "IncrFill"},
 
-           {text => "Blackouts"},
+           {text => "CfgEdit_Title_Blackouts"},
             {name => "BlackoutBadPingLimit"},
             {name => "BlackoutGoodCnt"},
             {name => "BlackoutPeriods"},
 
-           {text => "Other"},
+           {text => "CfgEdit_Title_Other"},
            {name => "PartialAgeMax"},
            {name => "RestoreInfoKeepCnt"},
            {name => "ArchiveInfoKeepCnt"},
@@ -278,9 +283,9 @@ our %ConfigMenu = (
        ],
     },
     backup => {
-        text => "Backup Settings",
+        text => "CfgEdit_Title_Backup_Settings",
         param => [
-           {text => "Client Lookup"},
+           {text => "CfgEdit_Title_Client_Lookup"},
            {name => "ClientNameAlias"},
            {name => "NmbLookupCmd"},
            {name => "NmbLookupFindHostCmd"},
@@ -288,12 +293,12 @@ our %ConfigMenu = (
            {name => "PingCmd"},
            {name => "PingMaxMsec"},
            
-           {text => "Other"},
+           {text => "CfgEdit_Title_Other"},
            {name => "ClientTimeout"},
            {name => "MaxOldPerPCLogFiles"},
            {name => "CompressLevel"},
 
-           {text => "User Commands"},
+           {text => "CfgEdit_Title_User_Commands"},
            {name => "DumpPreUserCmd"},
            {name => "DumpPostUserCmd"},
            {name => "DumpPreShareCmd"},
@@ -304,6 +309,14 @@ our %ConfigMenu = (
            {name => "ArchivePostUserCmd"},
        ],
     },
+    hosts => {
+        text => "CfgEdit_Title_Hosts",
+        param => [
+           {text    => "CfgEdit_Title_Hosts"},
+           {name    => "Hosts",
+             comment => "CfgEdit_Hosts_Comment"},
+        ],
+    },
 );
 
 sub action
@@ -318,14 +331,16 @@ sub action
     my $config_path = $host eq "" ? "$TopDir/conf/config.pl"
                                   : "$TopDir/pc/$host/config.pl";
 
-    my $Privileged = CheckPermission();
-    my $userHost = 1 if ( $Privileged && !$PrivAdmin && defined($host) );
+    my $Privileged = CheckPermission($host)
+                       && ($PrivAdmin || $Conf{CgiUserConfigEditEnable});
+    my $userHost = 1 if ( defined($host) );
+    my $debugText;
 
     if ( !$Privileged ) {
         ErrorExit(eval("qq{$Lang->{Only_privileged_users_can_edit_config_files}}"));
     }
 
-    if ( defined($In{menu}) || $In{editAction} eq "Save" ) {
+    if ( defined($In{menu}) || $In{editAction} eq $Lang->{CfgEdit_Button_Save} ) {
        $errors = errorCheck();
        if ( %$errors ) {
            #
@@ -334,18 +349,22 @@ sub action
            $In{editAction} = "";
             $In{newMenu} = "";
        }
+        if ( (my $var = $In{overrideUncheck}) ne "" ) {
+            #
+            # a compound variable was unchecked; delete extra
+            # variables to make the shape the same.
+            #
+            #print STDERR Dumper(\%In);
+            foreach my $v ( keys(%In) ) {
+                next if ( $v !~ /^v_z_(\Q$var\E(_z_.*|$))/ );
+                delete($In{$v}) if ( !defined($In{"orig_z_$1"}) );
+            }
+            delete($In{"vflds.$var"});
+        }
+
         ($newConf, $override) = inputParse($bpc, $userHost);
        $override = undef if ( $host eq "" );
 
-       #
-       # Copy all the orig_ input parameters
-       #
-       foreach my $var ( keys(%In) ) {
-           next if ( $var !~ /^orig_/ );
-           $contentHidden .= <<EOF;
-<input type="hidden" name="$var" value="${EscHTML($In{$var})}">
-EOF
-       }
     } else {
        #
        # First time: pick up the current config settings
@@ -358,28 +377,14 @@ EOF
                $override->{$param} = 1;
            }
        } else {
+            my $hostInfo = $bpc->HostInfoRead();
            $hostConf = {};
+            $mainConf->{Hosts} = [map($hostInfo->{$_}, sort(keys(%$hostInfo)))];
        }
        $newConf = { %$mainConf, %$hostConf };
-
-       #
-       # Emit all the original config settings
-       #
-       my $doneParam = {};
-        foreach my $param ( keys(%ConfigMeta) ) {
-            next if ( $doneParam->{$param} );
-            next if ( $userHost && !$bpc->{Conf}{CgiUserConfigEdit}{$param} );
-            $contentHidden .= fieldHiddenBuild($ConfigMeta{$param},
-                                    $param,
-                                    $mainConf->{$param},
-                                    "orig",
-                                );
-            $doneParam->{$param} = 1;
-       }
-
     }
 
-    if ( $In{editAction} ne "Save" && $In{newMenu} ne ""
+    if ( $In{editAction} ne $Lang->{CfgEdit_Button_Save} && $In{newMenu} ne ""
                    && defined($ConfigMenu{$In{newMenu}}) ) {
         $menu = $In{newMenu};
     }
@@ -389,7 +394,8 @@ EOF
         #
         # For a non-admin user editing the host config, we need to
         # figure out which subsets of the menu tree will be visible,
-        # based on what is enabled
+        # based on what is enabled.  Admin users can edit all the
+        # available per-host settings.
         #
         foreach my $m ( keys(%ConfigMenu) ) {
             my $enabled = 0;
@@ -403,10 +409,12 @@ EOF
                     $text = $n;
                     $mask[$text] = 1;
                 } else {
-                    if ( $bpc->{Conf}{CgiUserConfigEdit}{$param} ) {
+                    if ( $bpc->{Conf}{CgiUserConfigEdit}{$param}
+                          || (defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
+                                && $PrivAdmin) ) {
                         $mask[$text] = 0 if ( $text >= 0 );
                         $mask[$n] = 0;
-                        $enabled = 1;
+                        $enabled ||= 1;
                     } else {
                         $mask[$n] = 1;
                     }
@@ -432,47 +440,48 @@ EOF
     my $groupText;
     foreach my $m ( keys(%ConfigMenu) ) {
         next if ( $menuDisable{$m}{top} );
-       my $text = $ConfigMenu{$m}{text};
+       my $text = eval("qq($Lang->{$ConfigMenu{$m}{text}})");
         if ( $m eq $menu ) {
             $groupText .= <<EOF;
-<td bgcolor="grey"><a href="javascript:menuSubmit('$m')"><b>$text</b></a></td>
+<td class="editTabSel"><a href="javascript:menuSubmit('$m')"><b>$text</b></a></td>
 EOF
         } else {
             $groupText .= <<EOF;
-<td><a href="javascript:menuSubmit('$m')">$text</a></td>
+<td class="editTabNoSel"><a href="javascript:menuSubmit('$m')">$text</a></td>
 EOF
         }
     }
 
     if ( $host eq "" ) {
-       $content .= <<EOF;
-${h1("Main Configuration Editor")}
-EOF
+       $content .= eval("qq($Lang->{CfgEdit_Header_Main})");
     } else {
-       $content .= <<EOF;
-${h1("Host $host Configuration Editor")}
-<p>
-Note: Check Override if you want to modify a value specific to this host.
-EOF
+       $content .= eval("qq($Lang->{CfgEdit_Header_Host})");
     }
 
     my $saveDisplay = "block";
-    $saveDisplay = "none" if ( !$In{modified} );
+    $saveDisplay = "none" if ( !$In{modified}
+                          || $In{editAction} eq $Lang->{CfgEdit_Button_Save} );
+    #
+    # Add action and host to the URL so the nav bar link is
+    # highlighted
+    #
+    my $url = "$MyURL?action=editConfig";
+    $url .= "&host=$host" if ( $host ne "" );
     $content .= <<EOF;
 <table border="0" cellpadding="2">
 <tr>$groupText</tr>
 <tr>
-<form method="post" name="form1" action="$MyURL">
+<form method="post" name="form1" action="$url">
 <input type="hidden" name="host" value="$host">
 <input type="hidden" name="menu" value="$menu">
 <input type="hidden" name="newMenu" value="">
 <input type="hidden" name="modified" value="$In{modified}">
 <input type="hidden" name="deleteVar" value="">
 <input type="hidden" name="insertVar" value="">
+<input type="hidden" name="overrideUncheck" value="">
 <input type="hidden" name="addVar" value="">
 <input type="hidden" name="action" value="editConfig">
-<input type="submit" style="display: $saveDisplay" name="editAction" value="Save">
-$contentHidden
+<input type="submit" class="editSaveButton" style="display: $saveDisplay" name="editAction" value="${EscHTML($Lang->{CfgEdit_Button_Save})}">
 
 <script language="javascript" type="text/javascript">
 <!--
@@ -495,7 +504,8 @@ $contentHidden
 
     function addSubmit(varName, checkKey)
     {
-        if ( checkKey && document.form1.addVarKey.value == "" ) {
+        if ( checkKey
+            && eval("document.form1.addVarKey_" + varName + ".value") == "" ) {
             alert("New key must be non-empty");
             return;
         }
@@ -526,8 +536,8 @@ $contentHidden
            return false;
        }
        var allVars = {};
-       var varRE  = new RegExp("^v_(" + varName + ".*)");
-       var origRE = new RegExp("^orig_(" + varName + ".*)");
+       var varRE  = new RegExp("^v_z_(" + varName + ".*)");
+       var origRE = new RegExp("^orig_z_(" + varName + ".*)");
         for ( var i = 0 ; i < document.form1.elements.length ; i++ ) {
            var e = document.form1.elements[i];
            var re;
@@ -536,7 +546,7 @@ $contentHidden
                    allVars[re[1]] = 0;
                }
                allVars[re[1]]++;
-               //debugMsg("found v_ match with " + re[1]);
+               //debugMsg("found v_z_ match with " + re[1]);
                //debugMsg("allVars[" + re[1] + "] = " + allVars[re[1]]);
            } else if ( (re = origRE.exec(e.name)) != null ) {
                if ( allVars[re[1]] == null ) {
@@ -551,15 +561,18 @@ $contentHidden
            if ( allVars[v] != 0 ) {
                //debugMsg("Not the same shape because of " + v);
                sameShape = 0;
-           }
+           } else {
+                // copy the original variable values
+               //debugMsg("setting " + v);
+               eval("document.form1.v_z_" + v + ".value = document.form1.orig_z_" + v + ".value");
+            }
        }
        if ( sameShape ) {
-           for ( v in allVars ) {
-               //debugMsg("setting " + v);
-               eval("document.form1.v_" + v + ".value = document.form1.orig_" + v + ".value");
-           }
            return true;
        } else {
+            // need to rebuild the form since the compound variable
+            // has changed shape
+            document.form1.overrideUncheck.value = varName;
            document.form1.submit();
            return false;
        }
@@ -593,7 +606,7 @@ $contentHidden
 //-->
 </script>
 
-<span id="debug"></span>
+<span id="debug">$debugText</span>
 
 EOF
 
@@ -611,7 +624,7 @@ EOF
     if ( $In{deleteVar} ne "" && %$errors > 0 ) {
         my $matchAll = 1;
         foreach my $v ( keys(%$errors) ) {
-            if ( $v ne $In{deleteVar} && $v !~ /^\Q$In{deleteVar}_/ ) {
+            if ( $v ne $In{deleteVar} && $v !~ /^\Q$In{deleteVar}_z_/ ) {
                 $matchAll = 0;
                 last;
             }
@@ -621,28 +634,81 @@ EOF
 
     my $isError = %$errors;
 
-    if ( !$isError && $In{editAction} eq "Save" ) {
-        my $mesg;
+    if ( !$isError && $In{editAction} eq $Lang->{CfgEdit_Button_Save} ) {
+        my($mesg, $err);
        if ( $host ne "" ) {
            $hostConf = $bpc->ConfigDataRead($host) if ( !defined($hostConf) );
-            $mesg = configDiffMesg($host, $hostConf, $newConf);
-           foreach my $param ( %$newConf ) {
-               $hostConf->{$param} = $newConf->{$param}
-                               if ( $override->{param} );
+            my %hostConf2 = %$hostConf;
+           foreach my $param ( keys(%$newConf) ) {
+                if ( $override->{$param} ) {
+                    $hostConf->{$param} = $newConf->{$param}
+                } else {
+                    delete($hostConf->{$param});
+                }
            }
-           $bpc->ConfigDataWrite($host, $hostConf);
+            $mesg = configDiffMesg($host, \%hostConf2, $hostConf);
+           $err .= $bpc->ConfigDataWrite($host, $hostConf);
        } else {
            $mainConf = $bpc->ConfigDataRead() if ( !defined($mainConf) );
-            $mesg = configDiffMesg(undef, $mainConf, $newConf);
+
+            my $hostsSave = [];
+            my($hostsNew, $allHosts, $copyConf);
+            foreach my $entry ( @{$newConf->{Hosts}} ) {
+                next if ( $entry->{host} eq "" );
+                $allHosts->{$entry->{host}} = 1;
+                $allHosts->{$1} = 1 if ( $entry->{host} =~ /(.+?)\s*=/ );
+            }
+            foreach my $entry ( @{$newConf->{Hosts}} ) {
+                next if ( $entry->{host} eq ""
+                           || defined($hostsNew->{$entry->{host}}) );
+                if ( $entry->{host} =~ /(.+?)\s*=\s*(.+)/ ) {
+                    if ( defined($allHosts->{$2}) ) {
+                        $entry->{host} = $1;
+                        $copyConf->{$1} = $2;
+                    } else {
+                        my $fullHost = $entry->{host};
+                        my $copyHost = $2;
+                        $err .= eval("qq($Lang->{CfgEdit_Error_Copy_host_does_not_exist})");
+                    }
+                }
+                push(@$hostsSave, $entry);
+                $hostsNew->{$entry->{host}} = $entry;
+            }
+            ($mesg, my $hostChange) = hostsDiffMesg($hostsNew);
+            $bpc->HostInfoWrite($hostsNew) if ( $hostChange );
+            foreach my $host ( keys(%$copyConf) ) {
+                my $confData = $bpc->ConfigDataRead($copyConf->{$host});
+                my $fromHost = $copyConf->{$host};
+                $err  .= $bpc->ConfigDataWrite($host, $confData);
+                $mesg .= eval("qq($Lang->{CfgEdit_Log_Copy_host_config})");
+            }
+
+            delete($newConf->{Hosts});
+            $mesg .= configDiffMesg(undef, $mainConf, $newConf);
            $mainConf = { %$mainConf, %$newConf };
-           $bpc->ConfigDataWrite(undef, $mainConf);
+           $err .= $bpc->ConfigDataWrite(undef, $mainConf);
+            $newConf->{Hosts} = $hostsSave;
        }
+        if ( defined($err) ) {
+            $content .= <<EOF;
+<tr><td colspan="2" class="border"><span class="editError">$err</span></td></tr>
+EOF
+        }
+        $bpc->ServerConnect();
         if ( $mesg ne "" ) {
-            $bpc->ServerConnect();
+            (my $mesgBR = $mesg) =~ s/\n/<br>\n/g;
+            $content .= <<EOF;
+<tr><td colspan="2" class="border"><span class="editComment">$mesgBR</span></td></tr>
+EOF
             foreach my $str ( split(/\n/, $mesg) ) {
-                $bpc->ServerMesg($str);
+                $bpc->ServerMesg("log $str") if ( $str ne "" );
             }
         }
+        #
+        # Tell the server to reload, unless we only changed
+        # a client config
+        #
+        $bpc->ServerMesg("server reload") if ( $host eq "" );
     }
 
     my @mask = @{$menuDisable{$menu}{mask} || []};
@@ -658,7 +724,8 @@ EOF
             next;
         }
 
-       if ( defined(my $text = $paramInfo->{text}) ) {
+       if ( defined($paramInfo->{text}) ) {
+            my $text = eval("qq($Lang->{$paramInfo->{text}})");
            $content .= <<EOF;
 <tr><td colspan="2" class="editHeader">$text</td></tr>
 EOF
@@ -669,23 +736,30 @@ EOF
        # TODO: get parameter documentation
        #
        my $comment = "";
-       $comment =~ s/\'//g;
-       $comment =~ s/\"//g;
-        $comment =~ s/\n/ /g;
+       #$comment =~ s/\'//g;
+       #$comment =~ s/\"//g;
+        #$comment =~ s/\n/ /g;
 
         $doneParam->{$param} = 1;
 
         $content .= fieldEditBuild($ConfigMeta{$param},
-                                $param,
-                                $newConf->{$param},
-                                $errors,
-                                0,
-                                $comment,
-                                $isError,
-                                $paramInfo->{onchangeSubmit},
-                               defined($override) ? $param : undef,
-                               defined($override) ? $override->{$param} : undef
+                              $param,
+                              $newConf->{$param},
+                              $errors,
+                              0,
+                              $comment,
+                              $isError,
+                              $paramInfo->{onchangeSubmit},
+                             defined($override) ? $param : undef,
+                             defined($override) ? $override->{$param} : undef
                         );
+        if ( defined($paramInfo->{comment}) ) {
+            my $topDir = $bpc->TopDir;
+            my $text = eval("qq($Lang->{$paramInfo->{comment}})");
+           $content .= <<EOF;
+<tr><td colspan="2" class="editComment">$text</td></tr>
+EOF
+        }
     }
 
     #
@@ -693,7 +767,7 @@ EOF
     #
     foreach my $param ( sort(keys(%$errors)) ) {
        $content .= <<EOF;
-<tr><td colspan="2" class="border">$errors->{$param}</td></tr>
+<tr><td colspan="2" class="border"><span class="editError">$errors->{$param}</span></td></tr>
 EOF
        delete($errors->{$param});
     }
@@ -707,7 +781,10 @@ EOF
     #
     foreach my $param ( keys(%ConfigMeta) ) {
         next if ( $doneParam->{$param} );
-        next if ( $userHost && !$bpc->{Conf}{CgiUserConfigEdit}{$param} );
+        next if ( $userHost
+                      && (!defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
+                         || (!$PrivAdmin
+                             && !$bpc->{Conf}{CgiUserConfigEdit}{$param})) );
         $content .= fieldHiddenBuild($ConfigMeta{$param},
                             $param,
                             $newConf->{$param},
@@ -721,7 +798,60 @@ EOF
         $doneParam->{$param} = 1;
     }
 
+    if ( defined($In{menu}) || $In{editAction} eq $Lang->{CfgEdit_Button_Save} ) {
+        if ( $In{editAction} eq $Lang->{CfgEdit_Button_Save}
+                && !$userHost ) {
+            #
+            # Emit the new settings as orig_z_ parameters
+            #
+            $doneParam = {};
+            foreach my $param ( keys(%ConfigMeta) ) {
+                next if ( $doneParam->{$param} );
+                next if ( $userHost
+                          && (!defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
+                             || (!$PrivAdmin
+                                && !$bpc->{Conf}{CgiUserConfigEdit}{$param})) );
+                $contentHidden .= fieldHiddenBuild($ConfigMeta{$param},
+                                        $param,
+                                        $newConf->{$param},
+                                        "orig",
+                                    );
+                $doneParam->{$param} = 1;
+                $In{modified} = 0;
+            }
+        } else {
+            #
+            # Just switching menus: copy all the orig_z_ input parameters
+            #
+            foreach my $var ( keys(%In) ) {
+                next if ( $var !~ /^orig_z_/ );
+                $contentHidden .= <<EOF;
+<input type="hidden" name="$var" value="${EscHTML($In{$var})}">
+EOF
+            }
+       }
+    } else {
+       #
+       # First time: emit all the original config settings
+       #
+       $doneParam = {};
+        foreach my $param ( keys(%ConfigMeta) ) {
+            next if ( $doneParam->{$param} );
+            next if ( $userHost
+                          && (!defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
+                             || (!$PrivAdmin
+                                && !$bpc->{Conf}{CgiUserConfigEdit}{$param})) );
+            $contentHidden .= fieldHiddenBuild($ConfigMeta{$param},
+                                    $param,
+                                    $mainConf->{$param},
+                                    "orig",
+                                );
+            $doneParam->{$param} = 1;
+       }
+    }
+
     $content .= <<EOF;
+$contentHidden
 </form>
 </tr>
 </table>
@@ -743,14 +873,16 @@ sub fieldHiddenBuild
         $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
 
         for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
-            $content .= fieldHiddenBuild($type->{child}, "${varName}_$i",
+            $content .= fieldHiddenBuild($type->{child}, "${varName}_z_$i",
                                          $varValue->[$i], $prefix);
         }
-    } elsif ( $type->{type} eq "hash" ) {
+    } elsif ( $type->{type} eq "hash" || $type->{type} eq "horizHash" ) {
         $varValue = {} if ( ref($varValue) ne "HASH" );
         my(@order, $childType);
 
-        if ( defined($type->{child}) ) {
+        if ( defined($type->{order}) ) {
+            @order = @{$type->{order}};
+        } elsif ( defined($type->{child}) ) {
             @order = sort(keys(%{$type->{child}}));
         } else {
             @order = sort(keys(%$varValue));
@@ -769,18 +901,18 @@ sub fieldHiddenBuild
 <input type="hidden" name="vflds.$varName" value="${EscHTML($fld)}">
 EOF
             }
-            $content .= fieldHiddenBuild($childType, "${varName}_$fld",
+            $content .= fieldHiddenBuild($childType, "${varName}_z_$fld",
                                          $varValue->{$fld}, $prefix);
         }
     } elsif ( $type->{type} eq "shortlist" ) {
        $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
        $varValue = join(", ", @$varValue);
         $content .= <<EOF;
-<input type="hidden" name="${prefix}_$varName" value="${EscHTML($varValue)}">
+<input type="hidden" name="${prefix}_z_$varName" value="${EscHTML($varValue)}">
 EOF
     } else {
         $content .= <<EOF;
-<input type="hidden" name="${prefix}_$varName" value="${EscHTML($varValue)}">
+<input type="hidden" name="${prefix}_z_$varName" value="${EscHTML($varValue)}">
 EOF
     }
     return $content;
@@ -795,33 +927,42 @@ sub fieldEditBuild
     my $size = 50 - 10 * $level;
     $type = { type => $type } if ( ref($type) ne "HASH" );
 
+    $size = $type->{size} if ( defined($type->{size}) );
+
+    #
+    # These fragments allow inline conent to be turned on and off
+    #
+    # <tr><td colspan="2"><span id="id_$varName" style="display: none" class="editComment">$comment</span></td></tr>
+    # <tr><td class="border"><a href="javascript: displayHelp('$varName')">$varName</a>
+    #
+
     if ( $level == 0 ) {
+        my $lcVarName = lc($varName);
        $content .= <<EOF;
-<tr id="id_$varName" class="optionalComment"><td colspan="2">$comment</td></tr>
-<tr><td class="border"><a href="javascript: displayHelp('$varName')">$varName</a>
+<tr><td class="border"><a href="?action=view&type=docs#item_%24conf%7b$lcVarName%7d">$varName</a>
 EOF
        if ( defined($overrideVar) ) {
            my $override_checked = "";
-           if ( !$isError && $In{deleteVar}       =~ /^\Q${varName}_/
-                   || !$isError && $In{insertVar} =~ /^\Q${varName}\E(_|$)/
-                   || !$isError && $In{addVar}    =~ /^\Q${varName}\E(_|$)/ ) {
+           if ( !$isError && $In{deleteVar}      =~ /^\Q${varName}_z_/
+                  || !$isError && $In{insertVar} =~ /^\Q${varName}\E(_z_|$)/
+                  || !$isError && $In{addVar}    =~ /^\Q${varName}\E(_z_|$)/ ) {
                $overrideSet = 1;
            }
            if ( $overrideSet ) {
                $override_checked = "checked";
            }
             $content .= <<EOF;
-<br><input type="checkbox" name="override_$varName" $override_checked value="1" onClick="checkboxChange('$varName')">\&nbsp;Override
+<br><input type="checkbox" name="override_$varName" $override_checked value="1" onClick="checkboxChange('$varName')">\&nbsp;${EscHTML($Lang->{CfgEdit_Button_Override})}
 EOF
        }
        $content .= "</td>\n";
     }
 
-    $content .= "<td class=\"border\">\n";
     if ( $type->{type} eq "list" ) {
+        $content .= "<td class=\"border\">\n";
         $varValue = [] if ( !defined($varValue) );
         $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
-        if ( !$isError && $In{deleteVar} =~ /^\Q${varName}_\E(\d+)$/
+        if ( !$isError && $In{deleteVar} =~ /^\Q${varName}_z_\E(\d+)$/
                 && $1 < @$varValue ) {
             #
             # User deleted entry in this array
@@ -829,7 +970,7 @@ EOF
             splice(@$varValue, $1, 1) if ( @$varValue > 1 || $type->{emptyOk} );
             $In{deleteVar} = "";
         }
-        if ( !$isError && $In{insertVar} =~ /^\Q${varName}_\E(\d+)$/
+        if ( !$isError && $In{insertVar} =~ /^\Q${varName}_z_\E(\d+)$/
                 && $1 < @$varValue ) {
             #
             # User inserted entry in this array
@@ -847,36 +988,71 @@ EOF
         }
         $content .= "<table border=\"1\" cellspacing=\"0\">\n";
 
-        for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
-            $content .= "<tr><td class=\"border\">\n";
-           if ( @$varValue > 1 || $type->{emptyOk} ) {
-               $content .= <<EOF;
-<input type="button" name="ins_${varName}_$i" value="Insert"
-    onClick="insertSubmit('${varName}_$i')">
-<input type="button" name="del_${varName}_$i" value="Delete"
-    onClick="deleteSubmit('${varName}_$i')">
-EOF
-           }
-            $content .= "</td>\n";
-            $content .= fieldEditBuild($type->{child}, "${varName}_$i",
-                                $varValue->[$i], $errors, $level + 1, undef,
-                               $isError, $onchangeSubmit,
-                               $overrideVar, $overrideSet);
+        if ( ref($type) eq "HASH" && ref($type->{child}) eq "HASH"
+                    && $type->{child}{type} eq "horizHash" ) {
+            my @order;
+            if ( defined($type->{child}{order}) ) {
+                @order = @{$type->{child}{order}};
+            } else {
+                @order = sort(keys(%{$type->{child}{child}}));
+            }
+            $content .= "<tr><td class=\"border\"></td>\n";
+            for ( my $i = 0 ; $i < @order ; $i++ ) {
+                $content .= "<td>$order[$i]</td>\n";
+            }
             $content .= "</tr>\n";
+            for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
+                if ( @$varValue > 1 || $type->{emptyOk} ) {
+                    $content .= <<EOF;
+<td class="border">
+<input type="button" name="del_${varName}_z_$i" value="${EscHTML($Lang->{CfgEdit_Button_Delete})}"
+    onClick="deleteSubmit('${varName}_z_$i')">
+</td>
+EOF
+                }
+                $content .= fieldEditBuild($type->{child}, "${varName}_z_$i",
+                                  $varValue->[$i], $errors, $level + 1, undef,
+                                  $isError, $onchangeSubmit,
+                                  $overrideVar, $overrideSet);
+                $content .= "</tr>\n";
+            }
+        } else {
+            for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
+                $content .= <<EOF;
+<tr><td class="border">
+<input type="button" name="ins_${varName}_z_$i" value="${EscHTML($Lang->{CfgEdit_Button_Insert})}"
+    onClick="insertSubmit('${varName}_z_$i')">
+EOF
+                if ( @$varValue > 1 || $type->{emptyOk} ) {
+                    $content .= <<EOF;
+<input type="button" name="del_${varName}_z_$i" value="${EscHTML($Lang->{CfgEdit_Button_Delete})}"
+    onClick="deleteSubmit('${varName}_z_$i')">
+EOF
+                }
+                $content .= "</td>\n";
+                $content .= fieldEditBuild($type->{child}, "${varName}_z_$i",
+                                    $varValue->[$i], $errors, $level + 1, undef,
+                                    $isError, $onchangeSubmit,
+                                    $overrideVar, $overrideSet);
+                $content .= "</tr>\n";
+            }
         }
         $content .= <<EOF;
-<tr><td class="border"><input type="button" name="add_$varName" value="Add"
+<tr><td class="border"><input type="button" name="add_$varName" value="${EscHTML($Lang->{CfgEdit_Button_Add})}"
     onClick="addSubmit('$varName')"></td></tr>
 </table>
 EOF
+        $content .= "</td>\n";
     } elsif ( $type->{type} eq "hash" ) {
+        $content .= "<td class=\"border\">\n";
         $content .= "<table border=\"1\" cellspacing=\"0\">\n";
         $varValue = {} if ( ref($varValue) ne "HASH" );
 
         if ( !$isError && !$type->{noKeyEdit}
-                        && $In{deleteVar} =~ /^\Q${varName}_\E(\w+)$/ ) {
+                        && $In{deleteVar} !~ /^\Q${varName}_z_\E.*_z_/
+                        && $In{deleteVar} =~ /^\Q${varName}_z_\E(\w+)$/ ) {
             #
-            # User deleted entry in this array
+            # User deleted entry in this hash
             #
             delete($varValue->{$1}) if ( keys(%$varValue) > 1
                                            || $type->{emptyOk} );
@@ -887,13 +1063,15 @@ EOF
             #
             # User added entry to this array
             #
-            $varValue->{$In{addVarKey}} = ""
-                            if ( !defined($varValue->{$In{addVarKey}}) );
+            $varValue->{$In{"addVarKey_$varName"}} = ""
+                        if ( !defined($varValue->{$In{"addVarKey_$varName"}}) );
             $In{addVar} = "";
         }
         my(@order, $childType);
 
-        if ( defined($type->{child}) ) {
+        if ( defined($type->{order}) ) {
+            @order = @{$type->{order}};
+        } elsif ( defined($type->{child}) ) {
             @order = sort(keys(%{$type->{child}}));
         } else {
             @order = sort(keys(%$varValue));
@@ -906,8 +1084,8 @@ EOF
             if ( !$type->{noKeyEdit}
                    && (keys(%$varValue) > 1 || $type->{emptyOk}) ) {
                 $content .= <<EOF;
-<input type="submit" name="del_${varName}_$fld" value="Delete"
-        onClick="deleteSubmit('${varName}_$fld')">
+<input type="submit" name="del_${varName}_z_$fld" value="${EscHTML($Lang->{CfgEdit_Button_Delete})}"
+        onClick="deleteSubmit('${varName}_z_$fld')">
 EOF
             }
             if ( defined($type->{child}) ) {
@@ -923,7 +1101,7 @@ EOF
 EOF
             }
             $content .= "</td>\n";
-            $content .= fieldEditBuild($childType, "${varName}_$fld",
+            $content .= fieldEditBuild($childType, "${varName}_z_$fld",
                             $varValue->{$fld}, $errors, $level + 1, undef,
                            $isError, $onchangeSubmit,
                            $overrideVar, $overrideSet);
@@ -933,24 +1111,56 @@ EOF
         if ( !$type->{noKeyEdit} ) {
             $content .= <<EOF;
 <tr><td class="border" colspan="2">
-New key: <input type="text" name="addVarKey" size="20" maxlength="256" value="">
-<input type="button" name="add_$varName" value="Add" onClick="addSubmit('$varName', 1)">
+New key: <input type="text" name="addVarKey_$varName" size="20" maxlength="256" value="">
+<input type="button" name="add_$varName" value="${EscHTML($Lang->{CfgEdit_Button_Add})}" onClick="addSubmit('$varName', 1)">
 </td></tr>
 EOF
         }
         $content .= "</table>\n";
+        $content .= "</td>\n";
+    } elsif ( $type->{type} eq "horizHash" ) {
+        $varValue = {} if ( ref($varValue) ne "HASH" );
+        my(@order, $childType);
+
+        if ( defined($type->{order}) ) {
+            @order = @{$type->{order}};
+        } elsif ( defined($type->{child}) ) {
+            @order = sort(keys(%{$type->{child}}));
+        } else {
+            @order = sort(keys(%$varValue));
+        }
+
+        foreach my $fld ( @order ) {
+            if ( defined($type->{child}) ) {
+                $childType = $type->{child}{$fld};
+            } else {
+                $childType = $type->{childType};
+                #
+                # emit list of fields since they are user-defined
+                # rather than hard-coded
+                #
+                $content .= <<EOF;
+<input type="hidden" name="vflds.$varName" value="${EscHTML($fld)}">
+EOF
+            }
+            $content .= fieldEditBuild($childType, "${varName}_z_$fld",
+                            $varValue->{$fld}, $errors, $level + 1, undef,
+                           $isError, $onchangeSubmit,
+                           $overrideVar, $overrideSet);
+        }
     } else {
+        $content .= "<td class=\"border\">\n";
         if ( $isError ) {
             #
             # If there was an error, we use the original post values
             # in %In, rather than the parsed values in $varValue.
             # This is so that the user's erroneous input is preserved.
             #
-            $varValue = $In{"v_$varName"} if ( defined($In{"v_$varName"}) );
+            $varValue = $In{"v_z_$varName"} if ( defined($In{"v_z_$varName"}) );
         }
         if ( defined($errors->{$varName}) ) {
             $content .= <<EOF;
-$errors->{$varName}<br>
+<span class="editError">$errors->{$varName}</span><br>
 EOF
            delete($errors->{$varName});
         }
@@ -969,6 +1179,7 @@ EOF
         if ( $varValue !~ /\n/ &&
                ($type->{type} eq "integer"
                    || $type->{type} eq "string"
+                   || $type->{type} eq "execPath"
                    || $type->{type} eq "shortlist"
                    || $type->{type} eq "float") ) {
             # simple input box
@@ -977,17 +1188,17 @@ EOF
                $varValue = join(", ", @$varValue);
            }
             $content .= <<EOF;
-<input type="text" name="v_$varName" size="$size" maxlength="256" value="${EscHTML($varValue)}"$onChange>
+<input type="text" name="v_z_$varName" size="$size" maxlength="256" value="${EscHTML($varValue)}"$onChange>
 EOF
         } elsif ( $type->{type} eq "boolean" ) {
             # checkbox
             my $checked = "checked" if ( $varValue );
             $content .= <<EOF;
-<input type="checkbox" name="v_$varName" $checked value="1">
+<input type="checkbox" name="v_z_$varName" $checked value="1"$onChange>
 EOF
         } elsif ( $type->{type} eq "select" ) {
             $content .= <<EOF;
-<select name="v_$varName"$onChange>
+<select name="v_z_$varName"$onChange>
 EOF
             foreach my $option ( @{$type->{values}} ) {
                 my $sel = " selected" if ( $varValue eq $option );
@@ -999,11 +1210,11 @@ EOF
            my $rowCnt = $varValue =~ tr/\n//;
            $rowCnt = 1 if ( $rowCnt < 1 );
             $content .= <<EOF;
-<textarea name="v_$varName" cols="$size" rows="$rowCnt"$onChange>${EscHTML($varValue)}</textarea>
+<textarea name="v_z_$varName" cols="$size" rows="$rowCnt"$onChange>${EscHTML($varValue)}</textarea>
 EOF
         }
+        $content .= "</td>\n";
     }
-    $content .= "</td>\n";
     return $content;
 }
 
@@ -1025,13 +1236,15 @@ sub fieldErrorCheck
 
     if ( $type->{type} eq "list" ) {
         for ( my $i = 0 ; ; $i++ ) {
-            last if ( fieldErrorCheck($type->{child}, "${varName}_$i", $errors) );
+            last if ( fieldErrorCheck($type->{child}, "${varName}_z_$i", $errors) );
         }
-    } elsif ( $type->{type} eq "hash" ) {
+    } elsif ( $type->{type} eq "hash" || $type->{type} eq "horizHash" ) {
         my(@order, $childType);
         my $ret;
 
-        if ( defined($type->{child}) ) {
+        if ( defined($type->{order}) ) {
+            @order = @{$type->{order}};
+        } elsif ( defined($type->{child}) ) {
             @order = sort(keys(%{$type->{child}}));
         } else {
             @order = split(/\0/, $In{"vflds.$varName"});
@@ -1042,51 +1255,58 @@ sub fieldErrorCheck
             } else {
                 $childType = $type->{childType};
             }
-            $ret ||= fieldErrorCheck($childType, "${varName}_$fld", $errors);
+            $ret ||= fieldErrorCheck($childType, "${varName}_z_$fld", $errors);
         }
         return $ret;
     } else {
-        return 1 if ( !exists($In{"v_$varName"}) );
+        $In{"v_z_$varName"} = "0" if ( $type->{type} eq "boolean"
+                                        && $In{"v_z_$varName"} eq "" );
+
+        return 1 if ( !exists($In{"v_z_$varName"}) );
+
+        (my $var = $varName) =~ s/_z_/./g;
 
         if ( $type->{type} eq "integer"
                 || $type->{type} eq "boolean" ) {
-            if ( $In{"v_$varName"} !~ /^-?\d+\s*$/s
-                           && $In{"v_$varName"} ne "" ) {
-                $errors->{$varName} = "Error: $varName must be an integer";
+            if ( $In{"v_z_$varName"} !~ /^-?\d+\s*$/s
+                           && $In{"v_z_$varName"} ne "" ) {
+                $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__must_be_an_integer}}");
             }
         } elsif ( $type->{type} eq "float" ) {
-            if ( $In{"v_$varName"} !~ /^-?\d*(\.\d*)?\s*$/s
-                           && $In{"v_$varName"} ne "" ) {
+            if ( $In{"v_z_$varName"} !~ /^-?\d*(\.\d*)?\s*$/s
+                           && $In{"v_z_$varName"} ne "" ) {
                 $errors->{$varName}
-                        = "Error: $varName must be a real-valued number";
+                        = eval("qq{$Lang->{CfgEdit_Error__must_be_real_valued_number}}");
             }
         } elsif ( $type->{type} eq "shortlist" ) {
-           my @vals = split(/[,\s]+/, $In{"v_$varName"});
+           my @vals = split(/[,\s]+/, $In{"v_z_$varName"});
            for ( my $i = 0 ; $i < @vals ; $i++ ) {
                if ( $type->{child} eq "integer"
                        && $vals[$i] !~ /^-?\d+\s*$/s
                        && $vals[$i] ne "" ) {
                    my $k = $i + 1;
-                   $errors->{$varName} = "Error: $varName entry $k must"
-                                       . " be an integer";
+                   $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__entry__must_be_an_integer}}");
                } elsif ( $type->{child} eq "float"
                        && $vals[$i] !~ /^-?\d*(\.\d*)?\s*$/s
                        && $vals[$i] ne "" ) {
                    my $k = $i + 1;
-                   $errors->{$varName} = "Error: $varName entry $k must"
-                                       . " be a real-valued number";
+                   $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__entry__must_be_real_valued_number}}");
                }
            }
         } elsif ( $type->{type} eq "select" ) {
             my $match = 0;
             foreach my $option ( @{$type->{values}} ) {
-                if ( $In{"v_$varName"} eq $option ) {
+                if ( $In{"v_z_$varName"} eq $option ) {
                     $match = 1;
                     last;
                 }
             }
-            $errors->{$varName} = "Error: $varName must be a valid option"
+            $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__must_be_valid_option}}")
                             if ( !$match );
+        } elsif ( $type->{type} eq "execPath" ) {
+            if ( $In{"v_z_$varName"} ne "" && !-x $In{"v_z_$varName"} ) {
+                $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__must_be_executable_program}}");
+            }
         } else {
             #
             # $type->{type} eq "string": no error checking
@@ -1104,11 +1324,14 @@ sub inputParse
 
     foreach my $param ( keys(%ConfigMeta) ) {
         my $value;
-        next if ( $userHost && !$bpc->{Conf}{CgiUserConfigEdit}{$param} );
+        next if ( $userHost
+                      && (!defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
+                         || (!$PrivAdmin
+                            && !$bpc->{Conf}{CgiUserConfigEdit}{$param})) );
         fieldInputParse($ConfigMeta{$param}, $param, \$value);
         $conf->{$param}     = $value;
         $override->{$param} = $In{"override_$param"};
-}
+    }
     return ($conf, $override);
 }
 
@@ -1122,16 +1345,18 @@ sub fieldInputParse
         $$value = [];
         for ( my $i = 0 ; ; $i++ ) {
             my $val;
-            last if ( fieldInputParse($type->{child}, "${varName}_$i", \$val) );
+            last if ( fieldInputParse($type->{child}, "${varName}_z_$i", \$val) );
             push(@$$value, $val);
         }
         $$value = undef if ( $type->{undefIfEmpty} && @$$value == 0 );
-    } elsif ( $type->{type} eq "hash" ) {
+    } elsif ( $type->{type} eq "hash" || $type->{type} eq "horizHash" ) {
         my(@order, $childType);
         my $ret;
         $$value = {};
 
-        if ( defined($type->{child}) ) {
+        if ( defined($type->{order}) ) {
+            @order = @{$type->{order}};
+        } elsif ( defined($type->{child}) ) {
             @order = sort(keys(%{$type->{child}}));
         } else {
             @order = split(/\0/, $In{"vflds.$varName"});
@@ -1144,24 +1369,24 @@ sub fieldInputParse
             } else {
                 $childType = $type->{childType};
             }
-            $ret ||= fieldInputParse($childType, "${varName}_$fld", \$val);
+            $ret ||= fieldInputParse($childType, "${varName}_z_$fld", \$val);
             last if ( $ret );
             $$value->{$fld} = $val;
         }
         return $ret;
     } else {
         if ( $type->{type} eq "boolean" ) {
-            $$value = 0 + $In{"v_$varName"};
-        } elsif ( !exists($In{"v_$varName"}) ) {
+            $$value = 0 + $In{"v_z_$varName"};
+        } elsif ( !exists($In{"v_z_$varName"}) ) {
             return 1;
         }
 
         if ( $type->{type} eq "integer" ) {
-            $$value = 0 + $In{"v_$varName"};
+            $$value = 0 + $In{"v_z_$varName"};
         } elsif ( $type->{type} eq "float" ) {
-            $$value = 0 + $In{"v_$varName"};
+            $$value = 0 + $In{"v_z_$varName"};
         } elsif ( $type->{type} eq "shortlist" ) {
-            $$value = [split(/[,\s]+/, $In{"v_$varName"})];
+            $$value = [split(/[,\s]+/, $In{"v_z_$varName"})];
             if ( $type->{child} eq "float"
                     || $type->{child} eq "integer"
                     || $type->{child} eq "boolean" ) {
@@ -1170,7 +1395,8 @@ sub fieldInputParse
                 }
             }
         } else {
-            $$value = $In{"v_$varName"};
+            $$value = $In{"v_z_$varName"};
+            $$value =~ s/\r\n/\n/g;
         }
         $$value = undef if ( $type->{undefIfEmpty} && $$value eq "" );
     }
@@ -1193,14 +1419,16 @@ sub configDiffMesg
         if ( !exists($oldConf->{$p}) && !exists($newConf->{$p}) ) {
             next;
         } elsif ( exists($oldConf->{$p}) && !exists($newConf->{$p}) ) {
-            $mesg .= "log Deleted $p from $conf\n";
+            $mesg .= eval("qq($Lang->{CfgEdit_Log_Delete_param})");
         } elsif ( !exists($oldConf->{$p}) && exists($newConf->{$p}) ) {
             my $dump = Data::Dumper->new([$newConf->{$p}]);
             $dump->Indent(0);
             $dump->Sortkeys(1);
             $dump->Terse(1);
             my $value = $dump->Dump;
-            $mesg .= "log Added $p to $conf, set to $value\n";
+            $value =~ s/\n/\\n/g;
+            $value =~ s/\r/\\r/g;
+            $mesg .= eval("qq($Lang->{CfgEdit_Log_Add_param_value})");
         } else {
             my $dump = Data::Dumper->new([$newConf->{$p}]);
             $dump->Indent(0);
@@ -1218,11 +1446,53 @@ sub configDiffMesg
             $dump->Terse(1);
             my $valueOld = $dump->Dump;
 
-            $mesg .= "log Changed $p in $conf to $valueNew from $valueOld\n"
-                                    if ( $valueOld ne $valueNew );
+            (my $valueNew2 = $valueNew) =~ s/['\n\r]//g;
+            (my $valueOld2 = $valueOld) =~ s/['\n\r]//g;
+            $valueNew =~ s/\n/\\n/g;
+            $valueOld =~ s/\n/\\n/g;
+            $valueNew =~ s/\r/\\r/g;
+            $valueOld =~ s/\r/\\r/g;
+            $mesg .= eval("qq($Lang->{CfgEdit_Log_Change_param_value})")
+                                    if ( $valueOld2 ne $valueNew2 );
         }
     }
     return $mesg;
 }
 
+sub hostsDiffMesg
+{
+    my($hostsNew) = @_;
+    my $hostsOld = $bpc->HostInfoRead();
+    my($mesg, $hostChange);
+
+    foreach my $host ( keys(%$hostsOld) ) {
+        if ( !defined($hostsNew->{$host}) ) {
+            $mesg .= eval("qq($Lang->{CfgEdit_Log_Host_Delete})");
+            $hostChange++;
+            next;
+        }
+        foreach my $key ( keys(%{$hostsNew->{$host}}) ) {
+            next if ( $hostsNew->{$host}{$key} eq $hostsOld->{$host}{$key} );
+            my $valueOld = $hostsOld->{$host}{$key};
+            my $valueNew = $hostsNew->{$host}{$key};
+            $mesg .= eval("qq($Lang->{CfgEdit_Log_Host_Change})");
+            $hostChange++;
+        }
+    }
+
+    foreach my $host ( keys(%$hostsNew) ) {
+        next if ( defined($hostsOld->{$host}) );
+        my $dump = Data::Dumper->new([$hostsNew->{$host}]);
+        $dump->Indent(0);
+        $dump->Sortkeys(1);
+        $dump->Terse(1);
+        my $value = $dump->Dump;
+        $value =~ s/\n/\\n/g;
+        $value =~ s/\r/\\r/g;
+        $mesg .= eval("qq($Lang->{CfgEdit_Log_Host_Add})");
+        $hostChange++;
+    }
+    return ($mesg, $hostChange);
+}
+
 1;
index 70a7e49..dcabdb9 100644 (file)
@@ -95,7 +95,7 @@ sub NewRequest
 
     if ( !defined($bpc) ) {
        ErrorExit($Lang->{BackupPC__Lib__new_failed__check_apache_error_log})
-           if ( !($bpc = BackupPC::Lib->new(undef, undef, 1)) );
+           if ( !($bpc = BackupPC::Lib->new(undef, undef, undef, 1)) );
        $TopDir = $bpc->TopDir();
        $BinDir = $bpc->BinDir();
        %Conf   = $bpc->Conf();
@@ -220,7 +220,7 @@ sub EscHTML
     $s =~ s/\"/&quot;/g;
     $s =~ s/>/&gt;/g;
     $s =~ s/</&lt;/g;
-    $s =~ s{([^[:print:]])}{sprintf("&\#x%02X;", ord($1));}eg;
+    ### $s =~ s{([^[:print:]])}{sprintf("&\#x%02X;", ord($1));}eg;
     return \$s;
 }
 
@@ -409,31 +409,28 @@ sub Header
 {
     my($title, $content, $noBrowse, $contentSub, $contentPost) = @_;
     my @adminLinks = (
-        { link => "",                         name => $Lang->{Status}},
-        { link => "?action=adminOpts",        name => $Lang->{Admin_Options},
-                                              priv => 1},
-        { link => "?action=editConfig",       name => "Edit Config",
-                                              priv => 1},
-        { link => "?action=editHosts",        name => "Edit Hosts",
-                                              priv => 1},
-        { link => "?action=summary",          name => $Lang->{PC_Summary}},
-        { link => "?action=view&type=LOG",    name => $Lang->{LOG_file},
-                                              priv => 1},
-        { link => "?action=LOGlist",          name => $Lang->{Old_LOGs},
-                                              priv => 1},
-        { link => "?action=emailSummary",     name => $Lang->{Email_summary},
-                                              priv => 1},
-        { link => "?action=view&type=config", name => $Lang->{Config_file},
-                                              priv => 1},
-        { link => "?action=view&type=hosts",  name => $Lang->{Hosts_file},
-                                              priv => 1},
-        { link => "?action=queue",            name => $Lang->{Current_queues},
-                                              priv => 1},
+        { link => "",                      name => $Lang->{Status}},
+        { link => "?action=adminOpts",     name => $Lang->{Admin_Options},
+                                           priv => 1},
+        { link => "?action=editConfig",    name => $Lang->{CfgEdit_Edit_Config},
+                                           priv => 1},
+        { link => "?action=editConfig&newMenu=hosts",
+                                           name => $Lang->{CfgEdit_Edit_Hosts},
+                                           priv => 1},
+        { link => "?action=summary",       name => $Lang->{PC_Summary}},
+        { link => "?action=view&type=LOG", name => $Lang->{LOG_file},
+                                           priv => 1},
+        { link => "?action=LOGlist",       name => $Lang->{Old_LOGs},
+                                           priv => 1},
+        { link => "?action=emailSummary",  name => $Lang->{Email_summary},
+                                           priv => 1},
+        { link => "?action=queue",         name => $Lang->{Current_queues},
+                                           priv => 1},
         @{$Conf{CgiNavBarLinks} || []},
     );
     my $host = $In{host};
 
-    print $Cgi->header();
+    print $Cgi->header(-charset => "utf-8");
     print <<EOF;
 <!doctype html public "-//W3C//DTD HTML 4.01 Transitional//EN">
 <html><head>
@@ -469,11 +466,14 @@ EOF
                    $Lang->{Last_bad_XferLOG_errors_only},
                    " class=\"navbar\"");
        }
-        if ( -f "$TopDir/pc/$host/config.pl"
+        if ( $Conf{CgiUserConfigEditEnable} || $PrivAdmin ) {
+            NavLink("?action=editConfig&host=${EscURI($host)}",
+                    $Lang->{CfgEdit_Edit_Config}, " class=\"navbar\"");
+        } elsif ( -f "$TopDir/pc/$host/config.pl"
                     || ($host ne "config" && -f "$TopDir/conf/$host.pl") ) {
-           NavLink("?action=editConfig&host=${EscURI($host)}",
-                   "Edit Config", " class=\"navbar\"");
-       }
+            NavLink("?action=view&type=config&host=${EscURI($host)}",
+                    $Lang->{Config_file}, " class=\"navbar\"");
+        }
        print "</div>\n";
     }
     print("<div id=\"Content\">\n$content\n");
index e852c99..a07efeb 100644 (file)
@@ -76,7 +76,13 @@ sub action
         Trailer();
     } else {
         if ( $start ) {
-           my $ipAddr = ConfirmIPAddress($host);
+            $bpc->ConfigRead($host);
+            %Conf = $bpc->Conf();
+
+            my $checkHost = $host;
+            $checkHost = $Conf{ClientNameAlias}
+                                if ( $Conf{ClientNameAlias} ne "" );
+           my $ipAddr = ConfirmIPAddress($checkHost);
            my $content = eval("qq{$Lang->{Are_you_sure_start}}");
             Header(eval("qq{$Lang->{BackupPC__Start_Backup_Confirm_on__host}}"),$content);
         } else {
index 7995169..3d651be 100644 (file)
@@ -81,16 +81,16 @@ use vars qw(%ConfigMeta);
     BackupPCNightlyPeriod  => "integer",
     MaxOldLogFiles      => "integer",
 
-    SshPath            => {type => "string", undefIfEmpty => 1},
-    NmbLookupPath      => {type => "string", undefIfEmpty => 1},
-    PingPath           => {type => "string", undefIfEmpty => 1},
-    DfPath             => {type => "string", undefIfEmpty => 1},
+    SshPath            => {type => "execPath", undefIfEmpty => 1},
+    NmbLookupPath      => {type => "execPath", undefIfEmpty => 1},
+    PingPath           => {type => "execPath", undefIfEmpty => 1},
+    DfPath             => {type => "execPath", undefIfEmpty => 1},
     DfCmd              => "string",
-    SplitPath          => {type => "string", undefIfEmpty => 1},
-    ParPath            => {type => "string", undefIfEmpty => 1},
-    CatPath            => {type => "string", undefIfEmpty => 1},
-    GzipPath           => {type => "string", undefIfEmpty => 1},
-    Bzip2Path          => {type => "string", undefIfEmpty => 1},
+    SplitPath          => {type => "execPath", undefIfEmpty => 1},
+    ParPath            => {type => "execPath", undefIfEmpty => 1},
+    CatPath            => {type => "execPath", undefIfEmpty => 1},
+    GzipPath           => {type => "execPath", undefIfEmpty => 1},
+    Bzip2Path          => {type => "execPath", undefIfEmpty => 1},
     DfMaxUsagePct      => "float",
     TrashCleanSleepSec => "integer",
     DHCPAddressRanges   => {
@@ -99,6 +99,7 @@ use vars qw(%ConfigMeta);
             child   => {
                 type      => "hash",
                 noKeyEdit => 1,
+                order     => [qw(ipAddrBase first last)],
                 child     => {
                     ipAddrBase => "string",
                     first      => "integer",
@@ -109,7 +110,10 @@ use vars qw(%ConfigMeta);
     BackupPCUser       => "string",
     CgiDir             => "string",
     InstallDir         => "string",
-    BackupPCUserVerify  => "integer",
+    TopDir              => "string",
+    ConfDir             => "string",
+    LogDir              => "string",
+    BackupPCUserVerify  => "boolean",
     HardLinkMax                => "integer",
     PerlModuleLoad     => {
            type    => "list",
@@ -136,21 +140,27 @@ use vars qw(%ConfigMeta);
     IncrKeepCntMin     => "integer",
     IncrAgeMax         => "float",
     PartialAgeMax      => "float",
-    IncrFill           => "integer",
+    IncrFill           => "boolean",
     RestoreInfoKeepCnt => "integer",
     ArchiveInfoKeepCnt => "integer",
 
     BackupFilesOnly    => {
-           type         => "list",
-           emptyOk      => 1,
-           undefIfEmpty => 1,
-           child        => "string",
+            type      => "hash",
+            emptyOk   => 1,
+            childType => {
+                type      => "list",
+                emptyOk   => 1,
+                child     => "string",
+            },
     },
     BackupFilesExclude => {
-           type         => "list",
-           emptyOk      => 1,
-           undefIfEmpty => 1,
-           child        => "string",
+            type      => "hash",
+            emptyOk   => 1,
+            childType => {
+                type      => "list",
+                emptyOk   => 1,
+                child     => "string",
+            },
     },
 
     BlackoutBadPingLimit => "integer",
@@ -172,7 +182,7 @@ use vars qw(%ConfigMeta);
             },
         },
 
-    BackupZeroFilesIsFatal => "integer",
+    BackupZeroFilesIsFatal => "boolean",
 
     ######################################################################
     # How to backup a client
@@ -183,6 +193,8 @@ use vars qw(%ConfigMeta);
     },
     XferLogLevel       => "integer",
 
+    ClientCharset       => "string",
+
     SmbShareName       => {
            type   => "list",
            child  => "string",
@@ -214,7 +226,7 @@ use vars qw(%ConfigMeta);
 
     RsyncdClientPort   => "integer",
     RsyncdPasswd       => "string",
-    RsyncdAuthRequired => "integer",
+    RsyncdAuthRequired => "boolean",
 
     RsyncCsumCacheVerifyProb => "float",
     RsyncArgs          => {
@@ -233,14 +245,14 @@ use vars qw(%ConfigMeta);
            type   => "select",
            values => [qw(none bzip2 gzip)],
     },
-    ArchivePar         => "integer",
+    ArchivePar         => "boolean",
     ArchiveSplit       => "float",
     ArchiveClientCmd   => "string",
 
     NmbLookupCmd       => "string",
     NmbLookupFindHostCmd => "string",
 
-    FixedIPNetBiosNameCheck => "integer",
+    FixedIPNetBiosNameCheck => "boolean",
     PingCmd            => "string",
     PingMaxMsec                => "float",
 
@@ -265,7 +277,7 @@ use vars qw(%ConfigMeta);
     # Email reminders, status and messages
     # (can be overridden in the per-PC config.pl)
     ######################################################################
-    SendmailPath             => {type => "string", undefIfEmpty => 1},
+    SendmailPath             => {type => "execPath", undefIfEmpty => 1},
     EMailNotifyMinDays        => "float",
     EMailFromUserName         => "string",
     EMailAdminUserName        => "string",
@@ -278,6 +290,7 @@ use vars qw(%ConfigMeta);
     EMailNotifyOldOutlookDays => "float",
     EMailOutlookBackupSubj    => {type => "string",    undefIfEmpty => 1},
     EMailOutlookBackupMesg    => {type => "bigstring", undefIfEmpty => 1},
+    EMailHeaders              => {type => "bigstring", undefIfEmpty => 1},
 
     ######################################################################
     # CGI user interface configuration settings
@@ -285,12 +298,15 @@ use vars qw(%ConfigMeta);
     CgiAdminUserGroup  => "string",
     CgiAdminUsers      => "string",
     CgiURL             => "string",
-    Language           => "string",
+    Language           => {
+           type   => "select",
+           values => [qw(de en es fr it nl pt_br)],
+    },
     CgiUserHomePageCheck => "string",
     CgiUserUrlCreate    => "string",
-    CgiDateFormatMMDD  => "integer",
-    CgiNavBarAdminAllHosts => "integer",
-    CgiSearchBoxEnable         => "integer",
+    CgiDateFormatMMDD  => "boolean",
+    CgiNavBarAdminAllHosts => "boolean",
+    CgiSearchBoxEnable         => "boolean",
     CgiNavBarLinks     => {
            type    => "list",
            emptyOk => 1,
@@ -324,6 +340,7 @@ use vars qw(%ConfigMeta);
         },
     CgiImageDirURL     => "string",
     CgiCSSFile         => "string",
+    CgiUserConfigEditEnable => "boolean",
     CgiUserConfigEdit   => {
            type => "hash",
            noKeyEdit => 1,
@@ -348,6 +365,7 @@ use vars qw(%ConfigMeta);
                 BackupZeroFilesIsFatal    => "boolean",
                 XferMethod                => "boolean",
                 XferLogLevel              => "boolean",
+                ClientCharset             => "boolean",
                 SmbShareName              => "boolean",
                 SmbShareUserName          => "boolean",
                 SmbSharePasswd            => "boolean",
@@ -385,6 +403,25 @@ use vars qw(%ConfigMeta);
                 EMailOutlookBackupMesg    => "boolean",
            },
     },
+
+    ######################################################################
+    # Fake config setting for editing the hosts
+    ######################################################################
+    Hosts => {
+           type    => "list",
+           emptyOk => 1,
+           child   => {
+               type  => "horizHash",
+                order => [qw(host dhcp user moreUsers)],
+                noKeyEdit => 1,
+               child => {
+                   host       => { type => "string", size => 20 },
+                   dhcp       => { type => "boolean"            },
+                   user       => { type => "string", size => 20 },
+                   moreUsers  => { type => "string", size => 30 },
+               },
+           },
+    },
 );
 
 1;
index dc6e480..a5359a0 100644 (file)
@@ -29,7 +29,7 @@
 #
 #========================================================================
 #
-# Version 2.1.0, released 20 Jun 2004.
+# Version 2.1.1, released 13 Mar 2005.
 #
 # See http://backuppc.sourceforge.net.
 #
index c3fdb68..7b351c3 100644 (file)
@@ -1303,4 +1303,113 @@ EOF
 $Lang{howLong_not_been_backed_up} = "not been backed up successfully";
 $Lang{howLong_not_been_backed_up_for_days_days} = "not been backed up for \$days days";
 
+#######################################################################
+# Configuration editor strings
+#######################################################################
+
+$Lang{Only_privileged_users_can_edit_config_files} = "Only privileged users can edit configuation settings.";
+$Lang{CfgEdit_Edit_Config} = "Edit Config";
+$Lang{CfgEdit_Edit_Hosts}  = "Edit Hosts";
+
+$Lang{CfgEdit_Title_Server} = "Server";
+$Lang{CfgEdit_Title_General_Parameters} = "General Parameters";
+$Lang{CfgEdit_Title_Wakeup_Schedule} = "Wakeup Schedule";
+$Lang{CfgEdit_Title_Concurrent_Jobs} = "Concurrent Jobs";
+$Lang{CfgEdit_Title_Pool_Filesystem_Limits} = "Pool Filesystem Limits";
+$Lang{CfgEdit_Title_Other_Parameters} = "Other Parameters";
+$Lang{CfgEdit_Title_Remote_Apache_Settings} = "Remote Apache Settings";
+$Lang{CfgEdit_Title_Program_Paths} = "Program Paths";
+$Lang{CfgEdit_Title_Install_Paths} = "Install Paths";
+$Lang{CfgEdit_Title_Email} = "Email";
+$Lang{CfgEdit_Title_Email_settings} = "Email settings";
+$Lang{CfgEdit_Title_Email_User_Messages} = "Email User Messages";
+$Lang{CfgEdit_Title_CGI} = "CGI";
+$Lang{CfgEdit_Title_Admin_Privileges} = "Admin Privileges";
+$Lang{CfgEdit_Title_Page_Rendering} = "Page Rendering";
+$Lang{CfgEdit_Title_Paths} = "Paths";
+$Lang{CfgEdit_Title_User_URLs} = "User URLs";
+$Lang{CfgEdit_Title_User_Config_Editing} = "User Config Editing";
+$Lang{CfgEdit_Title_Xfer} = "Xfer";
+$Lang{CfgEdit_Title_Xfer_Settings} = "Xfer Settings";
+$Lang{CfgEdit_Title_Smb_Settings} = "Smb Settings";
+$Lang{CfgEdit_Title_Tar_Settings} = "Tar Settings";
+$Lang{CfgEdit_Title_Rsync_Settings} = "Rsync Settings";
+$Lang{CfgEdit_Title_Rsyncd_Settings} = "Rsyncd Settings";
+$Lang{CfgEdit_Title_Archive_Settings} = "Archive Settings";
+$Lang{CfgEdit_Title_Include_Exclude} = "Include/Exclude";
+$Lang{CfgEdit_Title_Smb_Paths_Commands} = "Smb Paths/Commands";
+$Lang{CfgEdit_Title_Tar_Paths_Commands} = "Tar Paths/Commands";
+$Lang{CfgEdit_Title_Rsync_Paths_Commands_Args} = "Rsync Paths/Commands/Args";
+$Lang{CfgEdit_Title_Rsyncd_Port_Args} = "Rsyncd Port/Args";
+$Lang{CfgEdit_Title_Archive_Paths_Commands} = "Archive Paths/Commands";
+$Lang{CfgEdit_Title_Schedule} = "Schedule";
+$Lang{CfgEdit_Title_Full_Backups} = "Full Backups";
+$Lang{CfgEdit_Title_Incremental_Backups} = "Incremental Backups";
+$Lang{CfgEdit_Title_Blackouts} = "Blackouts";
+$Lang{CfgEdit_Title_Other} = "Other";
+$Lang{CfgEdit_Title_Backup_Settings} = "Backup Settings";
+$Lang{CfgEdit_Title_Client_Lookup} = "Client Lookup";
+$Lang{CfgEdit_Title_Other} = "Other";
+$Lang{CfgEdit_Title_User_Commands} = "User Commands";
+$Lang{CfgEdit_Title_Hosts} = "Hosts";
+
+$Lang{CfgEdit_Hosts_Comment} = <<EOF;
+To add a new host, select Add and then enter the name.  To start with
+the per-host configuration from another host, enter the host name
+as NEWHOST=COPYHOST.  This will overwrite any existing per-host
+configuration for NEWHOST.  You can also do this for an existing
+host.  To delete a host, hit the Delete button.  For Add, Delete,
+and configuration copy, changes don't take effect until you select
+Save.  None of the deleted host's backups will be removed,
+so if you accidently delete a host, simply re-add it.  To completely
+remove a host's backups, you need to manually remove the files
+below \$topDir/pc/HOST
+EOF
+
+$Lang{CfgEdit_Header_Main} = <<EOF;
+\${h1("Main Configuration Editor")}
+EOF
+
+$Lang{CfgEdit_Header_Host} = <<EOF;
+\${h1("Host \$host Configuration Editor")}
+<p>
+Note: Check Override if you want to modify a value specific to this host.
+EOF
+
+$Lang{CfgEdit_Button_Save}     = "Save";
+$Lang{CfgEdit_Button_Insert}   = "Insert";
+$Lang{CfgEdit_Button_Delete}   = "Delete";
+$Lang{CfgEdit_Button_Add}      = "Add";
+$Lang{CfgEdit_Button_Override} = "Override";
+
+$Lang{CfgEdit_Error__must_be_an_integer}
+            = "Error: \$var must be an integer";
+$Lang{CfgEdit_Error__must_be_real_valued_number}
+            = "Error: \$var must be a real-valued number";
+$Lang{CfgEdit_Error__entry__must_be_an_integer}
+            = "Error: \$var entry \$k must be an integer";
+$Lang{CfgEdit_Error__entry__must_be_real_valued_number}
+            = "Error: \$var entry \$k must be a real-valued number";
+$Lang{CfgEdit_Error__must_be_executable_program}
+            = "Error: \$var must be a valid executable path";
+$Lang{CfgEdit_Error__must_be_valid_option}
+            = "Error: \$var must be a valid option";
+$Lang{CfgEdit_Error_Copy_host_does_not_exist}
+            = "Copy host \$copyHost doesn't exist; creating full host name \$fullHost.  Delete this host if that is not what you wanted.";
+
+$Lang{CfgEdit_Log_Copy_host_config}
+            = "\$User copied config from host \$fromHost to \$host\n";
+$Lang{CfgEdit_Log_Delete_param}
+            = "\$User deleted \$p from \$conf\n";
+$Lang{CfgEdit_Log_Add_param_value}
+            = "\$User added \$p to \$conf, set to \$value\n";
+$Lang{CfgEdit_Log_Change_param_value}
+            = "\$User changed \$p in \$conf to \$valueNew from \$valueOld\n";
+$Lang{CfgEdit_Log_Host_Delete}
+            = "\$User deleted host \$host\n";
+$Lang{CfgEdit_Log_Host_Change}
+            = "\$User host \$host changed \$key from \$valueOld to \$valueNew\n";
+$Lang{CfgEdit_Log_Host_Add}
+            = "\$User added host \$host: \$value\n";
+  
 #end of lang_en.pm
index 080261e..8e93c32 100644 (file)
@@ -40,16 +40,16 @@ $Lang{Admin_Options_Page} = <<EOF;
 EOF
 $Lang{Unable_to_connect_to_BackupPC_server} = "Impossible de se connecter au serveur BackupPC",
             "Ce script CGI (\$MyURL) est incapable de se connecter au serveur BackupPC"
-          . " sur \$Conf{ServerHost} au port \$Conf{ServerPort}. L'erreur"
+          . " sur \$Conf{ServerHost} au port \$Conf{ServerPort}.  L'erreur"
           . " est: \$err."
-          . " Il est possible que le serveur BackupPC ne fonctionne pas actuellement ou qu'il"
-          . " y ait une erreur de configuration. Veuillez contacter votre administrateur système.";
+          . " Il est possible que le serveur BackupPC ne roule pas ou qu'il y a une erreur "
+          . " de configuration. Veuillez contacter votre administrateur système.";
 $Lang{Admin_Start_Server} = <<EOF;
 \${h1(qq{$Lang{Unable_to_connect_to_BackupPC_server}})}
 <form action="\$MyURL" method="get">
-Le serveur BackupPC sur <tt>\$Conf{ServerHost}</tt>, port <tt>\$Conf{ServerPort}</tt>
+Le serveur BackupPC sur <tt>\$Conf{ServerHost}</tt> aur port <tt>\$Conf{ServerPort}</tt>
 n'est pas en fonction (vous l'avez peut-être arrêté, ou vous ne l'avez pas encore démarré).<br>
-Voulez-vous le démarrer?
+Voulez-vous le démarrer
 <input type="hidden" name="action" value="startServer">
 <input type="submit" value="Démarrer le serveur" name="ignore">
 </form>
@@ -75,7 +75,7 @@ $Lang{BackupPC_Server_Status_General_Info}= <<EOF;
         <li>\$numCmdQueue requêtes de commandes en attente,
         \$poolInfo
         <li>L\'espace de stockage a été récemment rempli à \$Info{DUlastValue}%
-            (\$DUlastTime), le maximum aujourd\'hui a été de \$Info{DUDailyMax}% (\$DUmaxTime)
+            (\$DUlastTime), le maximum d\'aujourd\'hui est \$Info{DUDailyMax}% (\$DUmaxTime)
             et hier le maximum était \$Info{DUDailyMaxPrev}%.
     </ul>
 </ul>
@@ -117,7 +117,7 @@ $Lang{BackupPC_Server_Status} = <<EOF;
 EOF
 
 # --------------------------------
-$Lang{BackupPC__Server_Summary} = "BackupPC: Bilan des machines";
+$Lang{BackupPC__Server_Summary} = "BackupPC: Bilan des PC";
 $Lang{BackupPC__Archive} = "BackupPC: Archivage";
 $Lang{BackupPC_Summary}=<<EOF;
 
@@ -198,7 +198,7 @@ $Lang{BackupPC_Archive}=<<EOF;
 //-->
 </script>
 
-Il y a \$hostCntGood hôtes qui ont été sauvegardés représentant \${fullSizeTot} Go
+Il y a \$hostCntGood hôtes qui ont étés sauvegardés représentant \${fullSizeTot} Go
 <p>
 <form name="form1" method="post" action="\$MyURL">
 <input type="hidden" name="fcbMax" value="\$checkBoxCnt">
@@ -291,7 +291,7 @@ La r
 Retourner à la page d\'accueil de <a href="\$MyURL?host=\$host">\$host</a>.
 EOF
 # --------------------------------
-$Lang{BackupPC__Start_Backup_Confirm_on__host} = "BackupPC: Confirmation du démarrage de la sauvegarde de \$host";
+$Lang{BackupPC__Start_Backup_Confirm_on__host} = "BackupPC: Confirmation du départ de la sauvegarde de \$host";
 # --------------------------------
 $Lang{Are_you_sure_start} = <<EOF;
 \${h1("Êtes vous certain ?")}
@@ -302,7 +302,7 @@ Vous allez bient
 <input type="hidden" name="host" value="\$host">
 <input type="hidden" name="hostIP" value="\$ipAddr">
 <input type="hidden" name="doit" value="1">
-Voulez-vous vraiment le faire ?
+Voulez vous vraiment le faire ?
 <input type="submit" value="\$In{action}" name="action">
 <input type="submit" value="Non" name="">
 </form>
@@ -323,7 +323,7 @@ Vous 
 En outre, prière de ne pas démarrer d\'autres sauvegarde pour
 <input type="text" name="backoff" size="10" value="\$backoff"> heures.
 <p>
-Voulez-vous vraiment le faire ?
+Voulez vous vraiment le faire ?
 <input type="submit" value="\$In{action}" name="action">
 <input type="submit" value="Non" name="">
 </form>
@@ -493,9 +493,9 @@ $Lang{Option_2__Download_Zip_archive} = <<EOF;
 Vous pouvez télécharger une archive compressée (.zip) contenant tous les fichiers/répertoires que vous 
 avez sélectionnés. Vous pouvez utiliser une application locale, comme Winzip, pour voir ou extraire n\'importe quel fichier.
 </p><p>
-<b>Attention:</b> en fonction des fichiers/répertoires que vous avez sélectionnés,
-cette archive peut devenir très très volumineuse. Cela peut prendre plusieurs minutes pour créer
-et transférer cette archive, et vous aurez besoin d\'assez d\'espace disque pour la stocker.
+<b>Attention:</b> en fonction de quels fichiers/répertoires vous avez sélectionné,
+cette archive peut devenir très très large.  Cela peut prendre plusieurs minutes pour créer
+et transférer cette archive, et vous aurez besoin d\'assez d\'espace disque pour le stocker.
 </p>
 <form action="\$MyURL" method="post">
 <input type="hidden" name="host" value="\${EscHTML(\$host)}">
@@ -538,8 +538,8 @@ que vous avez s
 comme tar ou winzip pour voir ou extraire n\'importe quel fichier.
 </p><p>
 <b>Attention:</b> en fonction des fichiers/répertoires que vous avez sélectionnés,
-cette archive peut devenir très très volumineuse.  Cela peut prendre plusieurs minutes
-pour créer et transférer l\'archive, et vous aurez besoin d\'assez
+cette archive peut devenir très très large.  Cela peut prendre plusieurs minutes
+pour créer et transférer l\'archive, et vous aurez besoin d'assez
 d\'espace disque local pour la stocker.
 </p>
 <form action="\$MyURL" method="post">
@@ -646,7 +646,7 @@ Cliquer sur le num
 \${h2("Résumé des erreurs de transfert")}
 <br><br>
 <table class="tableStnd" border cellspacing="1" cellpadding="3" width="80%">
-<tr class="tableheader"><td align="center"> Sauvegarde n° </td>
+<tr class="tableheader"><td align="center"> Nb sauvegarde </td>
     <td align="center"> Type </td>
     <td align="center"> Voir </td>
     <td align="center"> Nb erreurs transfert </td>
@@ -662,7 +662,7 @@ Cliquer sur le num
 <p>
 Les fichiers existants sont ceux qui sont déjà sur le serveur; 
 Les nouveaux fichiers sont ceux qui ont été ajoutés au serveur.
-Les fichiers vides et les erreurs de SMB ne sont pas comptabilisés dans les fichiers nouveaux ou réutilisés.
+Les fichiers vides et les erreurs de SMB ne sont pas comptabilisés parmi les nouveaux et les réutilisés.
 </p>
 <table glass="tableStnd" border cellspacing="1" cellpadding="3" width="80%">
 <tr class="tableheader"><td colspan="2" bgcolor="#ffffff"></td>
@@ -671,7 +671,7 @@ Les fichiers vides et les erreurs de SMB ne sont pas comptabilis
     <td align="center" colspan="2"> Nouveaux fichiers </td>
 </tr>
 <tr class="tableheader">
-    <td align="center"> Sauvegarde n° </td>
+    <td align="center"> Nb de sauvegarde  </td>
     <td align="center"> Type </td>
     <td align="center"> Nb de Fichiers </td>
     <td align="center"> Taille/Mo </td>
@@ -699,10 +699,10 @@ r
     <td align="center"> Type </td>
     <td align="center"> Niveau de Compression </td>
     <td align="center"> Taille/Mo </td>
-    <td align="center"> Taille compressée/Mo </td>
+    <td align="center"> Comp/Mo </td>
     <td align="center"> Compression </td>
     <td align="center"> Taille/Mo </td>
-    <td align="center"> Taille compressée/Mo </td>
+    <td align="center"> Comp/Mo </td>
     <td align="center"> Compression </td>
 </tr>
 \$compStr
@@ -712,7 +712,7 @@ EOF
 
 $Lang{Host__host_Archive_Summary} = "BackupPC: Résumé de l'archivage pour l'hôte \$host";
 $Lang{Host__host_Archive_Summary2} = <<EOF;
-\${h1("Résumé de l\'archivage pour l\'hôte \$host")}
+\${h1("Résumé de l'archivage pour l\'hôte \$host")}
 <p>
 \$warnStr
 <ul>
@@ -745,7 +745,7 @@ $Lang{NavSectionTitle_} = "Serveur";
 
 # -------------------------
 $Lang{Backup_browse_for__host} = <<EOF;
-\${h1("Navigation dans la sauvegarde de \$host")}
+\${h1("Navigation dans la sauvegarde pour \$host")}
 
 <script language="javascript" type="text/javascript">
 <!--
@@ -784,9 +784,9 @@ $Lang{Backup_browse_for__host} = <<EOF;
         (il y a \$backupAge jours),
 \$filledBackup
 <li> Entrez le répertoire: <input type="text" name="dir" size="50" maxlength="4096" value="\${EscHTML(\$dir)}"> <input type="submit" value="\$Lang->{Go}" name="Submit">
-<li> Cliquer sur un répertoire ci-dessous pour y naviguer,
-<li> Cliquer sur un fichier ci-dessous pour le restaurer,
-<li> Vous pouvez voir l'<a href="\$MyURL?action=dirHistory&host=\${EscURI(\$host)}&share=\$shareURI&dir=\$pathURI">historique</a> des différentes sauvegardes du répertoire courant.
+<li> Cliquer dans un répertoire ci-dessous pour y naviguer,
+<li> Cliquer dans un fichier ci-dessous pour le restaurer,
+<li> Vous pouvez l'<a href="\$MyURL?action=dirHistory&host=\${EscURI(\$host)}&share=\$shareURI&dir=\$pathURI">historique</a> de sauvegarde du répertoire courant.
 </ul>
 </form>
 
@@ -822,7 +822,7 @@ This is now in the checkAll row
 EOF
 
 # ------------------------------
-$Lang{DirHistory_backup_for__host} = "BackupPC: Historique des sauvegardes du répertoire courant pour \$host";
+$Lang{DirHistory_backup_for__host} = "BackupPC: Historique des sauvegardes de répertoires pour \$host";
 
 #
 # These two strings are used to build the links for directories and
@@ -832,15 +832,15 @@ $Lang{DirHistory_dirLink}  = "rep";
 $Lang{DirHistory_fileLink} = "v";
 
 $Lang{DirHistory_for__host} = <<EOF;
-\${h1("Historique des sauvegardes du répertoire courant pour \$host")}
+\${h1("Historique des sauvegardes de répertoires pour \$host")}
 <p>
-Cette page montre toutes les version disponibles des fichiers sauvegardés pour le répertoire courant :
+Cette page montre chaque version des fichiers parmi toutes sauvegardes:
 <ul>
 <li> Cliquez sur un numéro de sauvegarde pour revenir à la navigation de sauvegarde,
 <li> Cliquez sur un répertoire (\$Lang->{DirHistory_dirLink}) pour naviguer
      dans celui-ci.
 <li> Cliquez sur une version d'un fichier (\$Lang->{DirHistory_fileLink}0,
-     \$Lang->{DirHistory_fileLink}1, ...) pour le télécharger.
+     \$Lang->{DirHistory_fileLink}1, ...) pour la télécharger.
 <li> Les fichiers avec des contenus identiques pour plusieurs sauvegardes ont 
      le même numéro de version.
 <li> Les fichiers qui ne sont pas présents sur une sauvegarde en particulier 
@@ -879,7 +879,7 @@ $Lang{Restore___num_details_for__host2} = <<EOF;
 <tr><td class="tableheader"> Début </td><td class="border"> \$startTime </td></tr>
 <tr><td class="tableheader"> Durée </td><td class="border"> \$duration min </td></tr>
 <tr><td class="tableheader"> Nombre de fichier </td><td class="border"> \$Restores[\$i]{nFiles} </td></tr>
-<tr><td class="tableheader"> Taille totale </td><td class="border"> \${MB} Mo </td></tr>
+<tr><td class="tableheader"> Grosseur totale </td><td class="border"> \${MB} Mo </td></tr>
 <tr><td class="tableheader"> Taux de transfert </td><td class="border"> \$MBperSec Mo/s </td></tr>
 <tr><td class="tableheader"> Erreurs de TarCreate </td><td class="border"> \$Restores[\$i]{tarCreateErrs} </td></tr>
 <tr><td class="tableheader"> Erreurs de transfert </td><td class="border"> \$Restores[\$i]{xferErrs} </td></tr>
@@ -935,13 +935,13 @@ $Lang{BackupPC__Lib__new_failed__check_apache_error_log} = "BackupPC::Lib->new a
 $Lang{Wrong_user__my_userid_is___} =  
               "Mauvais utilisateur: mon userid est \$>, à la place de \$uid "
               . "(\$Conf{BackupPCUser})\n";
-#$Lang{Only_privileged_users_can_view_PC_summaries} = "Seuls les utilisateurs privilégiés peuvent voir les résumés des machines.";
+#$Lang{Only_privileged_users_can_view_PC_summaries} = "Seuls les utilisateurs privilégiés peuvent voir les résumés des PC.";
 $Lang{Only_privileged_users_can_stop_or_start_backups} = 
                   "Seuls les utilisateurs privilégiés peuvent arrêter ou démarrer des sauvegardes sur "
                   . " \${EscHTML(\$host)}.";
 $Lang{Invalid_number__num} = "Numéro invalide \$num";
 $Lang{Unable_to_open__file__configuration_problem} = "Impossible d\'ouvrir \$file: problème de configuration ?";
-$Lang{Only_privileged_users_can_view_log_or_config_files} = "Seuls les utilisateurs privilégiés peuvent voir les fichiers de journal ou les fichiers de configuration.";
+$Lang{Only_privileged_users_can_view_log_or_config_files} = "Seuls les utilisateurs privilégiés peuvent voir les fichier de jounal ou les fichiers de configuration.";
 $Lang{Only_privileged_users_can_view_log_files} = "Seuls les utilisateurs privilégiés peuvent voir les fichiers de journal.";
 $Lang{Only_privileged_users_can_view_email_summaries} = "Seuls les utilisateurs privilégiés peuvent voir les compte-rendu des courriels.";
 $Lang{Only_privileged_users_can_browse_backup_files} = "Seuls les utilisateurs privilégiés peuvent parcourir les fichiers de sauvegarde"
@@ -954,10 +954,10 @@ $Lang{Can_t_browse_bad_directory_name2} = "Ne peut pas parcourir "
 $Lang{Only_privileged_users_can_restore_backup_files} = "Seuls les utilisateurs privilégiés peuvent restaurer "
                 . " des fichiers de sauvegarde pour l\'hôte \${EscHTML(\$In{host})}.";
 $Lang{Bad_host_name} = "Mauvais nom d\'hôte \${EscHTML(\$host)}";
-$Lang{You_haven_t_selected_any_files__please_go_Back_to} = "Vous n\'avez sélectionné aucun fichier ; "
+$Lang{You_haven_t_selected_any_files__please_go_Back_to} = "Vous n'avez sélectionné aucun fichier; "
     . "vous pouvez revenir en arrière pour sélectionner des fichiers.";
-$Lang{You_haven_t_selected_any_hosts} = "Vous n\'avez sélectionné aucun hôte ; veuillez retourner à la page précédente pour"
-                . " faire la sélection d\'un hôte.";
+$Lang{You_haven_t_selected_any_hosts} = "Vous avez sélectionné aucun hôte; veuillez retourné à la page précédente pour"
+                . " faire la sélection d'un hôte.";
 $Lang{Nice_try__but_you_can_t_put} = "Bien tenté, mais vous ne pouvez pas mettre \'..\' dans n\'importe quel nom de fichier.";
 $Lang{Host__doesn_t_exist} = "L'hôte \${EscHTML(\$In{hostDest})} n\'existe pas.";
 $Lang{You_don_t_have_permission_to_restore_onto_host} = "Vous n\'avez pas la permission de restaurer sur l\'hôte"
@@ -972,9 +972,9 @@ $Lang{Only_privileged_users_can_view_information_about} = "Seuls les utilisateur
                 . " informations sur l\'hôte \${EscHTML(\$host)}." ;
 $Lang{Only_privileged_users_can_view_archive_information} = "Seuls les utilisateurs privilégiés peuvent voir les informations d'archivage.";
 $Lang{Only_privileged_users_can_view_restore_information} = "Seuls les utilisateurs privilégiés peuvent restaurer des informations.";
-$Lang{Restore_number__num_for_host__does_not_exist} = "La restauration numéro \$num de l\'hôte \${EscHTML(\$host)} n\'existe pas";
+$Lang{Restore_number__num_for_host__does_not_exist} = "Restauration numéro \$num de l\'hôte \${EscHTML(\$host)} n\'existe pas";
 
-$Lang{Archive_number__num_for_host__does_not_exist} = "L\'archive n°\$num pour l\'hôte \${EscHTML(\$host)} n\'existe pas.";
+$Lang{Archive_number__num_for_host__does_not_exist} = "L'archive n°\$num pour l'hôte \${EscHTML(\$host)} n'existe pas.";
 
 $Lang{Can_t_find_IP_address_for} = "Ne peut pas trouver d\'adresse IP pour \${EscHTML(\$host)}";
 $Lang{host_is_a_DHCP_host} = <<EOF;
@@ -1003,7 +1003,7 @@ $Lang{Archive_requested} = "Archivage demand
 # -------------------------------------------------
 
 $Lang{Status} = "État";
-$Lang{PC_Summary} = "Bilan des machines";
+$Lang{PC_Summary} = "Bilan des PC";
 $Lang{LOG_file} = "Fichier journal";
 $Lang{LOG_files} = "Fichiers journaux";
 $Lang{Old_LOGs} = "Vieux journaux";
@@ -1019,8 +1019,8 @@ $Lang{Hosts} = "H
 $Lang{Select_a_host} = "Choisissez un hôte...";
 
 $Lang{There_have_been_no_archives} = "<h2> Il n'y a pas d'archives </h2>\n";
-$Lang{This_PC_has_never_been_backed_up} = "<h2> Cette machine n'a jamais été sauvegardée !! </h2>\n";
-$Lang{This_PC_is_used_by} = "<li>Cette machine est utilisée par \${UserLink(\$user)}";
+$Lang{This_PC_has_never_been_backed_up} = "<h2> Ce PC n'a jamais été sauvegardé !! </h2>\n";
+$Lang{This_PC_is_used_by} = "<li>Ce PC est utilisé par \${UserLink(\$user)}";
 
 $Lang{Extracting_only_Errors} = "(Extraction des erreurs seulement)";
 $Lang{XferLOG} = "JournalXfer";
@@ -1028,7 +1028,7 @@ $Lang{Errors}  = "Erreurs";
 
 # ------------
 $Lang{Last_email_sent_to__was_at___subject} = <<EOF;
-<li>Le dernier courriel envoyé à \${UserLink(\$user)} le \$mailTime, avait comme sujet "\$subj".
+<li>Dernier courriel envoyé à \${UserLink(\$user)} le \$mailTime, avait comme sujet "\$subj".
 EOF
 # ------------
 $Lang{The_command_cmd_is_currently_running_for_started} = <<EOF;
@@ -1047,7 +1047,7 @@ EOF
 
 # ---------
 $Lang{A_command_for_host_is_on_the_command_queue_will_run_soon} = <<EOF;
-<li>Une commande pour l\'hôte \$host est dans la liste d\'attente des commandes (sera lancée bientôt).
+<li>Une commande pour l\'hôte \$host est dans la liste d\'attente des commandes (sera lancé bientôt).
 EOF
 
 # --------
@@ -1075,8 +1075,8 @@ $Lang{priorStr_to_host_have_succeeded_StatusHostaliveCnt_consecutive_times} = <<
 EOF
 
 $Lang{Because__host_has_been_on_the_network_at_least__Conf_BlackoutGoodCnt_consecutive_times___} = <<EOF;
-<li>\$host a été présent sur le réseau au moins \$Conf{BlackoutGoodCnt}
-fois consécutives, il ne sera donc pas sauvegardé de \$blackoutStr.
+<li>Du fait que \$host a été présent sur le réseau au moins \$Conf{BlackoutGoodCnt}
+fois consécutives, il ne sera pas sauvegardé de \$blackoutStr.
 EOF
 
 $Lang{__time0_to__time1_on__days} = "\$t0 à \$t1 pendant \$days";
@@ -1118,15 +1118,15 @@ EOF
 
 $Lang{Home} = "Accueil";
 $Lang{Browse} = "Explorer les sauvegardes";
-$Lang{Last_bad_XferLOG} = "Bilan des derniers transferts échoués";
-$Lang{Last_bad_XferLOG_errors_only} = "Bilan des derniers transferts échoués (erreurs&nbsp;seulement)";
+$Lang{Last_bad_XferLOG} = "Dernier bilan des transferts échouées";
+$Lang{Last_bad_XferLOG_errors_only} = "Dernier bilan des transferts échouées (erreurs&nbsp;seulement)";
 
 $Lang{This_display_is_merged_with_backup} = <<EOF;
 <li> Cet affichage est fusionné avec la sauvegarde n°\$numF, la plus récente copie intégrale.
 EOF
 
 $Lang{Visit_this_directory_in_backup} = <<EOF;
-<li> Choisissez la sauvegarde que vous désirez voir : <select onChange="window.location=this.value">\$otherDirs </select>
+<li> Choisissez la sauvegarde que vous désirez voir: <select onChange="window.location=this.value">\$otherDirs </select>
 EOF
 
 $Lang{Restore_Summary} = <<EOF;
@@ -1139,7 +1139,7 @@ Cliquer sur le num
     <td align="right"> Date de départ</td>
     <td align="right"> Durée/min</td>
     <td align="right"> Nb fichiers </td>
-    <td align="right"> Taille/Mo </td>
+    <td align="right"> Mo </td>
     <td align="right"> Nb errs tar </td>
     <td align="right"> Nb errs trans </td>
 </tr>
@@ -1198,8 +1198,8 @@ $Lang{Reason_backup_done}    = "sauvegarde termin
 $Lang{Reason_restore_done}   = "restauration terminée";
 $Lang{Reason_archive_done}   = "archivage terminé";
 $Lang{Reason_nothing_to_do}  = "rien à faire";
-$Lang{Reason_backup_failed}  = "la sauvegarde a échoué";
-$Lang{Reason_restore_failed} = "la restauration a échoué";
+$Lang{Reason_backup_failed}  = "la sauvegarde a échouée";
+$Lang{Reason_restore_failed} = "la restauration a échouée";
 $Lang{Reason_archive_failed} = "l'archivage a échoué";
 $Lang{Reason_no_ping}        = "pas de ping";
 $Lang{Reason_backup_canceled_by_user}  = "sauvegarde annulée par l'utilisateur";
@@ -1219,9 +1219,9 @@ $headers
 $userName,
 
 Notre logiciel de copies de sécurité n'a jamais réussi à
-effectuer la sauvegarde de votre ordinateur ($host). Les sauvegardes
+prendre de sauvegarde de votre ordinateur ($host). Les sauvegardes
 devraient normalement survenir lorsque votre ordinateur est connecté
-au réseau. Vous devriez contacter le support informatique si :
+au réseau. Vous devriez contacter le support informatique si:
 
   - Votre ordinateur est régulièrement connecté au réseau, ce qui
     signifie qu'il y aurait un problème de configuration
@@ -1231,8 +1231,8 @@ au r
     votre ordinateur ni ne voulez recevoir d'autres messages
     comme celui-ci.
 
-Dans le cas contraire, veuillez vous assurer que votre ordinateur est 
-connecté au réseau lorsque ce sera possible.
+Autrement, veuillez vous assurer que votre ordinateur est connecté
+au réseau lorsque ce sera possible.
 
 Merci de votre attention,
 BackupPC Génie
@@ -1240,7 +1240,7 @@ http://backuppc.sourceforge.net
 EOF
 
 # No recent backup
-$Lang{EMailNoBackupRecentSubj} = "BackupPC: aucune sauvegarde récente de \$host";
+$Lang{EMailNoBackupRecentSubj} = "BackupPC: auncune sauvegarde récente de \$host";
 $Lang{EMailNoBackupRecentMesg} = <<'EOF';
 To: $user$domain
 cc:
@@ -1250,7 +1250,7 @@ $userName,
 
 Aucune sauvegarde de votre ordinateur n'a été effectuée depuis $days
 jours. $numBackups sauvegardes ont étés effectuées du $firstTime
-jusqu'à il y a $days jours. Les sauvegardes devraient normalement
+jusqu'il y à $days jours. Les sauvegardes devraient normalement
 survenir lorsque votre ordinateur est connecté au réseau.
 
 Si votre ordinateur a été connecté au réseau plus de quelques heures
@@ -1259,9 +1259,9 @@ informatique pour savoir pourquoi les sauvegardes ne s'effectuent pas.
 
 Autrement, si vous êtes en dehors du bureau, il n'y a pas d'autres
 choses que vous pouvez faire, à part faire des copies de vos fichiers
-importants sur d'autres medias. Vous devez réaliser que tout fichier crée
+importants sur d'autres media. Vous devez réaliser que tout fichier crée
 ou modifié durant les $days derniers jours (incluant les courriels et
-les fichiers attachés) ne pourra pas être restauré si un problème survient
+les fichiers attachés) ne pourra être restauré si une problème survient
 avec votre ordinateur.
 
 Merci de votre attention,
@@ -1285,15 +1285,15 @@ au $lastTime.  Par contre, Outlook bloque ses fichiers lorsqu'il est
 ouvert, ce qui empêche de les sauvegarder.
 
 Il est recommandé d'effectuer une sauvegarde de vos fichiers Outlook
-quand vous serez connecté au réseau en quittant Outlook et toute autre
-application, et en visitant ce lien avec votre navigateur web:
+quand vous serez connecté au réseau en quittant Outlook et tout autre
+application, et en visitant ce lien avec votre fureteur web:
 
     $CgiURL?host=$host               
 
 Choisissez "Démarrer la sauvegarde incrémentielle" deux fois afin
 d'effectuer une nouvelle sauvegarde. Vous pouvez ensuite choisir
 "Retourner à la page de $host" et appuyer sur "Recharger" dans votre
-navigateur avec de vérifier le bon fonctionnement de la sauvegarde. La
+fureteur avec de vérifier le bon fonctionnement de la sauvegarde. La
 sauvegarde devrait prendre quelques minutes à s'effectuer.
 
 Merci de votre attention,
@@ -1301,7 +1301,7 @@ BackupPC G
 http://backuppc.sourceforge.net
 EOF
 
-$Lang{howLong_not_been_backed_up} = "jamais été sauvegardés";
+$Lang{howLong_not_been_backed_up} = "jamais étés sauvegardés";
 $Lang{howLong_not_been_backed_up_for_days_days} = "pas été sauvegardés depuis \$days jours";
 
 #end of lang_fr.pm
index 642d70e..93ec0f0 100644 (file)
@@ -1,6 +1,6 @@
 #!/bin/perl
 #
-# $Id: it.pm,v 1.11 2005/09/11 01:37:02 cbarratt Exp $
+# $Id: it.pm,v 1.12 2006/01/24 07:10:57 cbarratt Exp $
 #
 # Italian i18n file
 #
index 1e430ca..6805642 100644 (file)
@@ -54,23 +54,47 @@ use Config;
 sub new
 {
     my $class = shift;
-    my($topDir, $installDir, $noUserCheck) = @_;
+    my($topDir, $installDir, $confDir, $noUserCheck) = @_;
 
-    my $paths = {
-        TopDir  => $topDir || '/data/BackupPC',
-        BinDir  => $installDir || '/usr/local/BackupPC',
-        LibDir  => $installDir || '/usr/local/BackupPC',
-    };
-    $paths->{BinDir} .= "/bin";
-    $paths->{LibDir} .= "/lib";
+    #
+    # Whether to use filesystem hierarchy standard for file layout.
+    # If set, text config files are below /etc/BackupPC.
+    #
+    my $useFSH = 0;
+    my $paths;
 
-    $paths->{storage} = BackupPC::Storage->new($paths);
+    #
+    # 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 ( $useFSH ) {
+        $paths = {
+            useFSH  => $useFSH,
+            TopDir  => $topDir || '/data/BackupPC',
+            BinDir  => $installDir ? "$installDir/bin" : '/usr/local/BackupPC/bin',
+            LibDir  => $installDir ? "$installDir/lib" : '/usr/local/BackupPC/lib',
+            ConfDir => $confDir || '/etc/BackupPC',
+            LogDir  => $topDir     ? "$topDir/log" : '/var/log/BackupPC',
+        };
+    } else {
+        $paths = {
+            useFSH  => $useFSH,
+            TopDir  => $topDir || '/data/BackupPC',
+            BinDir  => $installDir ? "$installDir/bin" : '/usr/local/BackupPC/bin',
+            LibDir  => $installDir ? "$installDir/lib" : '/usr/local/BackupPC/lib',
+            ConfDir => $topDir     ? "$topDir/conf" : '/data/BackupPC/conf',
+            LogDir  => $topDir     ? "$topDir/log" : '/data/BackupPC/log',
+        };
+    }
 
     my $bpc = bless {
        %$paths,
         Version => '2.1.0',
     }, $class;
 
+    $bpc->{storage} = BackupPC::Storage->new($paths);
+
     #
     # Clean up %ENV and setup other variables.
     #
@@ -82,6 +106,15 @@ sub new
         return;
     }
 
+    #
+    # Update the paths based on the config file
+    #
+    foreach my $dir ( qw(TopDir BinDir LibDir ConfDir LogDir) ) {
+        next if ( !defined($bpc->{Conf}{$dir}) );
+        $paths->{$dir} = $bpc->{$dir} = $bpc->{Conf}{$dir};
+    }
+    $bpc->{storage}->setPaths($paths);
+
     #
     # Verify we are running as the correct user
     #
@@ -107,6 +140,30 @@ sub BinDir
     return $bpc->{BinDir};
 }
 
+sub LogDir
+{
+    my($bpc) = @_;
+    return $bpc->{LogDir};
+}
+
+sub ConfDir
+{
+    my($bpc) = @_;
+    return $bpc->{ConfDir};
+}
+
+sub LibDir
+{
+    my($bpc) = @_;
+    return $bpc->{LibDir};
+}
+
+sub useFHS
+{
+    my($bpc) = @_;
+    return $bpc->{useFHS};
+}
+
 sub Version
 {
     my($bpc) = @_;
@@ -319,6 +376,13 @@ sub HostInfoRead
     return $bpc->{storage}->HostInfoRead($host);
 }
 
+sub HostInfoWrite
+{
+    my($bpc, $host) = @_;
+
+    return $bpc->{storage}->HostInfoWrite($host);
+}
+
 #
 # Return the mtime of the hosts file
 #
@@ -1111,10 +1175,9 @@ sub cmdSystemOrEval
     return $bpc->cmdSystemOrEvalLong($cmd, $stdoutCB, 0, undef, @args);
 }
 
-
 #
 # Promotes $conf->{BackupFilesOnly}, $conf->{BackupFilesExclude}
-# to hashes and $conf->{$shareName} to an array
+# to hashes and $conf->{$shareName} to an array.
 #
 sub backupFileConfFix
 {
@@ -1123,10 +1186,25 @@ sub backupFileConfFix
     $conf->{$shareName} = [ $conf->{$shareName} ]
                     if ( ref($conf->{$shareName}) ne "ARRAY" );
     foreach my $param qw(BackupFilesOnly BackupFilesExclude) {
-        next if ( !defined($conf->{$param}) || ref($conf->{$param}) eq "HASH" );
-        $conf->{$param} = [ $conf->{$param} ]
-                               if ( ref($conf->{$param}) ne "ARRAY" );
-        $conf->{$param} = { map { $_ => $conf->{$param} }                                                       @{$conf->{$shareName}} };
+        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}} };
+        }
     }
 }
 
index d0be0e6..7360e7c 100644 (file)
@@ -494,4 +494,53 @@ sub filePartialCompare
     return 1;
 }
 
+#
+# LinkOrCopy() does a hardlink from oldFile to newFile.
+#
+# If that fails (because there are too many links on oldFile)
+# then oldFile is copied to newFile, and the pool stats are
+# returned to be added to the new file list.  That allows
+# BackupPC_link to try again, and to create a new pool file
+# if necessary.
+#
+sub LinkOrCopy
+{
+    my($bpc, $oldFile, $oldFileComp, $newFile, $newFileComp) = @_;
+    my($nRead, $data);
+
+    unlink($newFile)  if ( -f $newFile );
+    #
+    # Try to link if hardlink limit is ok, and compression types
+    # are the same
+    #
+    return (1, undef) if ( (stat($oldFile))[3] < $bpc->{Conf}{HardLinkMax}
+                            && !$oldFileComp == !$newFileComp
+                            && link($oldFile, $newFile) );
+    #
+    # There are too many links on oldFile, or compression
+    # type if different, so now we have to copy it.
+    #
+    # We need to compute the file size, which is expensive
+    # since we need to read the file twice.  That's probably
+    # ok since the hardlink limit is rarely hit.
+    #
+    my $readFd = BackupPC::FileZIO->open($oldFile, 0, $oldFileComp);
+    if ( !defined($readFd) ) {
+        return (0, undef, undef, undef, ["LinkOrCopy: can't open $oldFile"]);
+    }
+    while ( $readFd->read(\$data, $BufSize) > 0 ) {
+        $nRead += length($data);
+    }
+    $readFd->rewind();
+
+    my $poolWrite = BackupPC::PoolWrite->new($bpc, $newFile,
+                                             $nRead, $newFileComp);
+    while ( $readFd->read(\$data, $BufSize) > 0 ) {
+        $poolWrite->write(\$data);
+    }
+    my($exists, $digest, $outSize, $errs) = $poolWrite->close;
+
+    return ($exists, $digest, $nRead, $outSize, $errs);
+}
+
 1;
index 7d142f6..73d758e 100644 (file)
@@ -39,6 +39,7 @@ package BackupPC::Storage;
 
 use strict;
 use BackupPC::Storage::Text;
+use Data::Dumper;
 
 sub new
 {
@@ -51,6 +52,7 @@ sub new
                     xferErrs xferBadFile xferBadShare tarErrs
                     compress sizeExistComp sizeNewComp
                     noFill fillFromNum mangle xferMethod level
+                    charset
                 )],
         RestoreFields => [qw(
                     num startTime endTime result errorMsg nFiles size
@@ -64,4 +66,24 @@ sub new
     return BackupPC::Storage::Text->new($flds, $paths, @_);
 }
 
+#
+# Writes per-backup information into the pc/nnn/backupInfo
+# file to allow later recovery of the pc/backups file in
+# cases when it is corrupted.
+#
+sub backupInfoWrite
+{
+    my($class, $pcDir, $bkupNum, $bkupInfo, $force) = @_;
+
+    return if ( !$force && -f "$pcDir/$bkupNum/backupInfo" );
+    my($dump) = Data::Dumper->new(
+             [   $bkupInfo],
+             [qw(*backupInfo)]);
+    $dump->Indent(1);
+    if ( open(BKUPINFO, ">", "$pcDir/$bkupNum/backupInfo") ) {
+        print(BKUPINFO $dump->Dump);
+        close(BKUPINFO);
+    }
+}
+
 1;
index a93fb61..286b6c0 100644 (file)
@@ -41,6 +41,7 @@ package BackupPC::Storage::Text;
 use strict;
 use vars qw(%Conf);
 use Data::Dumper;
+use File::Path;
 use Fcntl qw/:flock/;
 
 sub new
@@ -55,6 +56,16 @@ sub new
     return $s;
 }
 
+sub setPaths
+{
+    my $class = shift;
+    my($paths) = @_;
+
+    foreach my $v ( keys(%$paths) ) {
+        $class->{$v} = $paths->{$v};
+    }
+}
+
 sub BackupInfoRead
 {
     my($s, $host) = @_;
@@ -66,7 +77,7 @@ sub BackupInfoRead
        binmode(BK_INFO);
         while ( <BK_INFO> ) {
             s/[\n\r]+//;
-            next if ( !/^(\d+\t(incr|full|partial)[\d\t]*$)/ );
+            next if ( !/^(\d+\t(incr|full|partial).*)/ );
             $_ = $1;
             @{$Backups[@Backups]}{@{$s->{BackupFields}}} = split(/\t/);
         }
@@ -79,26 +90,20 @@ sub BackupInfoRead
 sub BackupInfoWrite
 {
     my($s, $host, @Backups) = @_;
-    local(*BK_INFO, *LOCK);
-    my($i);
+    my($i, $contents, $fileOk);
 
-    flock(LOCK, LOCK_EX) if open(LOCK, "$s->{TopDir}/pc/$host/LOCK");
-    if ( -s "$s->{TopDir}/pc/$host/backups" ) {
-       unlink("$s->{TopDir}/pc/$host/backups.old")
-                   if ( -f "$s->{TopDir}/pc/$host/backups.old" );
-       rename("$s->{TopDir}/pc/$host/backups",
-              "$s->{TopDir}/pc/$host/backups.old")
-                   if ( -f "$s->{TopDir}/pc/$host/backups" );
-    }
-    if ( open(BK_INFO, ">$s->{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{@{$s->{BackupFields}}}));
-        }
-        close(BK_INFO);
+    #
+    # Generate the file contents
+    #
+    for ( $i = 0 ; $i < @Backups ; $i++ ) {
+        my %b = %{$Backups[$i]};
+        $contents .= join("\t", @b{@{$s->{BackupFields}}}) . "\n";
     }
-    close(LOCK);
+    
+    #
+    # Write the file
+    #
+    return $s->TextFileWrite("$s->{TopDir}/pc/$host", "backups", $contents);
 }
 
 sub RestoreInfoRead
@@ -126,26 +131,20 @@ sub RestoreInfoWrite
 {
     my($s, $host, @Restores) = @_;
     local(*RESTORE_INFO, *LOCK);
-    my($i);
+    my($i, $contents, $fileOk);
 
-    flock(LOCK, LOCK_EX) if open(LOCK, "$s->{TopDir}/pc/$host/LOCK");
-    if ( -s "$s->{TopDir}/pc/$host/restores" ) {
-       unlink("$s->{TopDir}/pc/$host/restores.old")
-                   if ( -f "$s->{TopDir}/pc/$host/restores.old" );
-       rename("$s->{TopDir}/pc/$host/restores",
-              "$s->{TopDir}/pc/$host/restores.old")
-                   if ( -f "$s->{TopDir}/pc/$host/restores" );
-    }
-    if ( open(RESTORE_INFO, ">$s->{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{@{$s->{RestoreFields}}}));
-        }
-        close(RESTORE_INFO);
+    #
+    # Generate the file contents
+    #
+    for ( $i = 0 ; $i < @Restores ; $i++ ) {
+        my %b = %{$Restores[$i]};
+        $contents .= join("\t", @b{@{$s->{RestoreFields}}}) . "\n";
     }
-    close(LOCK);
+
+    #
+    # Write the file
+    #
+    return $s->TextFileWrite("$s->{TopDir}/pc/$host", "restores", $contents);
 }
 
 sub ArchiveInfoRead
@@ -173,26 +172,71 @@ sub ArchiveInfoWrite
 {
     my($s, $host, @Archives) = @_;
     local(*ARCHIVE_INFO, *LOCK);
-    my($i);
+    my($i, $contents, $fileOk);
 
-    flock(LOCK, LOCK_EX) if open(LOCK, "$s->{TopDir}/pc/$host/LOCK");
-    if ( -s "$s->{TopDir}/pc/$host/archives" ) {
-       unlink("$s->{TopDir}/pc/$host/archives.old")
-                   if ( -f "$s->{TopDir}/pc/$host/archives.old" );
-       rename("$s->{TopDir}/pc/$host/archives",
-              "$s->{TopDir}/pc/$host/archives.old")
-                   if ( -f "$s->{TopDir}/pc/$host/archives" );
+    #
+    # Generate the file contents
+    #
+    for ( $i = 0 ; $i < @Archives ; $i++ ) {
+        my %b = %{$Archives[$i]};
+        $contents .= join("\t", @b{@{$s->{ArchiveFields}}}) . "\n";
     }
-    if ( open(ARCHIVE_INFO, ">$s->{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{@{$s->{ArchiveFields}}}));
+
+    #
+    # Write the file
+    #
+    return $s->TextFileWrite("$s->{TopDir}/pc/$host", "archives", $contents);
+}
+
+#
+# Write a text file as safely as possible.  We write to
+# a new file, verify the file, and the rename the file.
+# The previous version of the file is renamed with a
+# .old extension.
+#
+sub TextFileWrite
+{
+    my($s, $dir, $file, $contents) = @_;
+    local(*FD, *LOCK);
+    my($fileOk);
+
+    mkpath($dir, 0, 0775) if ( !-d $dir );
+    if ( open(FD, ">", "$dir/$file.new") ) {
+       binmode(FD);
+        print FD $contents;
+        close(FD);
+        #
+        # verify the file
+        #
+        if ( open(FD, "<", "$dir/$file.new") ) {
+            binmode(FD);
+            if ( join("", <FD>) ne $contents ) {
+                return "TextFileWrite: Failed to verify $dir/$file.new";
+            } else {
+                $fileOk = 1;
+            }
+            close(FD);
         }
-        close(ARCHIVE_INFO);
     }
-    close(LOCK);
+    if ( $fileOk ) {
+        my $lock;
+        
+        if ( open(LOCK, "$dir/LOCK") || open(LOCK, ">", "$dir/LOCK") ) {
+            $lock = 1;
+            flock(LOCK, LOCK_EX);
+        }
+        if ( -s "$dir/$file" ) {
+            unlink("$dir/$file.old")               if ( -f "$dir/$file.old" );
+            rename("$dir/$file", "$dir/$file.old") if ( -f "$dir/$file" );
+        } else {
+            unlink("$dir/$file") if ( -f "$dir/$file" );
+        }
+        rename("$dir/$file.new", "$dir/$file") if ( -f "$dir/$file.new" );
+        close(LOCK) if ( $lock );
+    } else {
+        return "TextFileWrite: Failed to write $dir/$file.new";
+    }
+    return;
 }
 
 sub ConfigDataRead
@@ -223,6 +267,16 @@ sub ConfigDataRead
         }
         %$conf = ( %$conf, %Conf );
     }
+    #
+    # Promote BackupFilesOnly and BackupFilesExclude to hashes
+    #
+    foreach my $param qw(BackupFilesOnly BackupFilesExclude) {
+        next if ( !defined($conf->{$param}) || ref($conf->{$param}) eq "HASH" );
+        $conf->{$param} = [ $conf->{$param} ]
+                                if ( ref($conf->{$param}) ne "ARRAY" );
+        $conf->{$param} = { "*" => $conf->{$param} };
+    }
+
     return (undef, $conf);
 }
 
@@ -230,69 +284,79 @@ sub ConfigDataWrite
 {
     my($s, $host, $newConf) = @_;
 
-    my($confPath) = $host eq "" ? "$s->{TopDir}/conf/config.pl"
-                               : "$s->{TopDir}/pc/$host/config.pl";
+    my($confDir) = $host eq "" ? "$s->{TopDir}/conf"
+                              : "$s->{TopDir}/pc/$host";
 
-    my $err = $s->ConfigFileMerge($confPath, "$confPath.new", $newConf);
-    #
-    # TODO: add lock and rename
-    #
+    my($err, $contents) = $s->ConfigFileMerge("$confDir/config.pl", $newConf);
+    if ( defined($err) ) {
+        return $err;
+    } else {
+        #
+        # Write the file
+        #
+        return $s->TextFileWrite($confDir, "config.pl", $contents);
+    }
 }
 
 sub ConfigFileMerge
 {
-    my($s, $inFile, $outFile, $newConf) = @_;
-
-    open(C, $inFile) || return "ConfigFileMerge: can't open/read $inFile";
-    binmode(C);
-
-    open(OUT, ">", $outFile)
-                    || return "ConfigFileMerge: can't open/write $outFile";
-    binmode(OUT);
-
-    my($out);
+    my($s, $inFile, $newConf) = @_;
+    local(*C);
+    my($contents, $out);
     my $comment = 1;
     my $skipVar = 0;
     my $endLine = undef;
     my $done = {};
 
-    while ( <C> ) {
-       if ( $comment && /^\s*#/ ) {
-           $out .= $_;
-       } elsif ( /^\s*\$Conf\{([^}]*)\}\s*=/ ) {
-           my $var = $1;
-           if ( exists($newConf->{$var}) ) { 
-               print OUT $out;
-               my $d = Data::Dumper->new([$newConf->{$var}], [*value]);
-               $d->Indent(1);
-               $d->Terse(1);
-               my $value = $d->Dump;
-               $value =~ s/(.*)\n/$1;\n/s;
-               print OUT "\$Conf{$var} = ", $value;
-               $done->{$var} = 1;
-           }
-           $endLine = $1 if ( /^\s*\$Conf\{[^}]*} *= *<<(.*);/ );
-           $endLine = $1 if ( /^\s*\$Conf\{[^}]*} *= *<<'(.*)';/ );
-           $out = "";
-           $skipVar = 1;
-       } elsif ( $skipVar ) {
-           if ( !defined($endLine) && (/^\s*[\r\n]*$/ || /^\s*#/) ) {
-               $skipVar = 0;
-               $comment = 1;
-               $out .= $_;
-           }
-           if ( defined($endLine) && /^\Q$endLine\E[\n\r]*$/ ) {
-               $endLine = undef;
-               $skipVar = 0;
-               $comment = 1;
-           }
-       } else {
-           $out .= $_;
-       }
-    }
-    if ( $out ne "" ) {
-       print OUT $out;
+    if ( -f $inFile ) {
+        #
+        # Match existing settings in current config file
+        #
+        open(C, $inFile)
+            || return ("ConfigFileMerge: can't open/read $inFile", undef);
+        binmode(C);
+
+        while ( <C> ) {
+            if ( $comment && /^\s*#/ ) {
+                $out .= $_;
+            } elsif ( /^\s*\$Conf\{([^}]*)\}\s*=/ ) {
+                my $var = $1;
+                if ( exists($newConf->{$var}) ) { 
+                    $contents .= $out;
+                    my $d = Data::Dumper->new([$newConf->{$var}], [*value]);
+                    $d->Indent(1);
+                    $d->Terse(1);
+                    my $value = $d->Dump;
+                    $value =~ s/(.*)\n/$1;\n/s;
+                    $contents .= "\$Conf{$var} = " . $value;
+                    $done->{$var} = 1;
+                }
+                $endLine = $1 if ( /^\s*\$Conf\{[^}]*} *= *<<(.*);/ );
+                $endLine = $1 if ( /^\s*\$Conf\{[^}]*} *= *<<'(.*)';/ );
+                $out = "";
+                $skipVar = 1;
+            } elsif ( $skipVar ) {
+                if ( !defined($endLine) && (/^\s*[\r\n]*$/ || /^\s*#/) ) {
+                    $skipVar = 0;
+                    $comment = 1;
+                    $out .= $_;
+                }
+                if ( defined($endLine) && /^\Q$endLine\E[\n\r]*$/ ) {
+                    $endLine = undef;
+                    $skipVar = 0;
+                    $comment = 1;
+                }
+            } else {
+                $out .= $_;
+            }
+        }
+        close(C);
+        $contents .= $out;
     }
+
+    #
+    # Add new entries not matched in current config file
+    #
     foreach my $var ( sort(keys(%$newConf)) ) {
        next if ( $done->{$var} );
        my $d = Data::Dumper->new([$newConf->{$var}], [*value]);
@@ -300,11 +364,10 @@ sub ConfigFileMerge
        $d->Terse(1);
        my $value = $d->Dump;
        $value =~ s/(.*)\n/$1;\n/s;
-       print OUT "\$Conf{$var} = ", $value;
+       $contents .= "\$Conf{$var} = " . $value;
        $done->{$var} = 1;
     }
-    close(C);
-    close(OUT);
+    return (undef, $contents);
 }
 
 #
@@ -327,11 +390,12 @@ sub HostInfoRead
 {
     my($s, $host) = @_;
     my(%hosts, @hdr, @fld);
-    local(*HOST_INFO);
+    local(*HOST_INFO, *LOCK);
 
+    flock(LOCK, LOCK_EX) if open(LOCK, "$s->{TopDir}/pc/$host/LOCK");
     if ( !open(HOST_INFO, "$s->{TopDir}/conf/hosts") ) {
-        print(STDERR $s->timeStamp,
-                     "Can't open $s->{TopDir}/conf/hosts\n");
+        print(STDERR "Can't open $s->{TopDir}/conf/hosts\n");
+        close(LOCK);
         return {};
     }
     binmode(HOST_INFO);
@@ -354,21 +418,84 @@ sub HostInfoRead
         }
         if ( @hdr ) {
             if ( defined($host) ) {
-                next if ( lc($fld[0]) ne $host );
-                @{$hosts{lc($fld[0])}}{@hdr} = @fld;
+                next if ( lc($fld[0]) ne lc($host) );
+                @{$hosts{$fld[0]}}{@hdr} = @fld;
                close(HOST_INFO);
+                close(LOCK);
                 return \%hosts;
             } else {
-                @{$hosts{lc($fld[0])}}{@hdr} = @fld;
+                @{$hosts{$fld[0]}}{@hdr} = @fld;
             }
         } else {
             @hdr = @fld;
         }
     }
     close(HOST_INFO);
+    close(LOCK);
     return \%hosts;
 }
 
+#
+# Writes new hosts information to the hosts file in $s->{TopDir}/conf/hosts.
+# With no argument a ref to a hash of hosts is returned.  Each
+# hash contains fields as specified in the hosts file.  With an
+# argument a ref to a single hash is returned with information
+# for just that host.
+#
+sub HostInfoWrite
+{
+    my($s, $hosts) = @_;
+    my($gotHdr, @fld, $hostText, $contents);
+    local(*HOST_INFO);
+
+    if ( !open(HOST_INFO, "$s->{TopDir}/conf/hosts") ) {
+        return "Can't open $s->{TopDir}/conf/hosts";
+    }
+    foreach my $host ( keys(%$hosts) ) {
+        my $name = "$hosts->{$host}{host}";
+        my $rest = "\t$hosts->{$host}{dhcp}"
+                 . "\t$hosts->{$host}{user}"
+                 . "\t$hosts->{$host}{moreUsers}";
+        $name =~ s/ /\\ /g;
+        $rest =~ s/ //g;
+        $hostText->{$host} = $name . $rest;
+    }
+    binmode(HOST_INFO);
+    while ( <HOST_INFO> ) {
+        s/[\n\r]+//;
+        if ( /^\s*$/ || /^\s*#/ ) {
+            $contents .= $_ . "\n";
+            next;
+        }
+        if ( !$gotHdr ) {
+            $contents .= $_ . "\n";
+            $gotHdr = 1;
+            next;
+        }
+        @fld = split(/(?<!\\)\s+/, $1);
+        #
+        # Remove any \
+        #
+        foreach ( @fld ) {
+            s{\\(\s)}{$1}g;
+        }
+        if ( defined($hostText->{$fld[0]}) ) {
+            $contents .= $hostText->{$fld[0]} . "\n";
+            delete($hostText->{$fld[0]});
+        }
+    }
+    foreach my $host ( sort(keys(%$hostText)) ) {
+        $contents .= $hostText->{$host} . "\n";
+        delete($hostText->{$host});
+    }
+    close(HOST_INFO);
+
+    #
+    # Write and verify the new host file
+    #
+    return $s->TextFileWrite("$s->{TopDir}/conf", "hosts", $contents);
+}
+
 #
 # Return the mtime of the hosts file
 #
index f1a043a..6dc0f8d 100644 (file)
@@ -150,6 +150,7 @@ sub dirCache
             next if ( defined($m->{files}{$fileUM})
                    || $file eq ".."
                    || $file eq "."
+                   || $file eq "backupInfo"
                    || $mangle && $file eq "attrib" );
            #
            # skip directories in earlier backups (each backup always
index 45f4f3b..4088f17 100644 (file)
@@ -303,6 +303,7 @@ sub start
        pidHandler   => sub {
                            $t->{pidHandler}(@_);
                        },
+        clientCharset => $conf->{ClientCharset},
        fio          => BackupPC::Xfer::RsyncFileIO->new({
                            xfer       => $t,
                            bpc        => $t->{bpc},
@@ -316,6 +317,7 @@ sub start
                                              $t->{XferLOG}->write(\$str);
                                          },
                            cacheCheckProb => $conf->{RsyncCsumCacheVerifyProb},
+                            clientCharset  => $conf->{ClientCharset},
                            %$fioArgs,
                      }),
     });
index c0cf21b..ef736e1 100644 (file)
@@ -22,6 +22,7 @@ package BackupPC::Xfer::RsyncFileIO;
 
 use strict;
 use File::Path;
+use Encode qw/from_to/;
 use BackupPC::Attrib qw(:all);
 use BackupPC::View;
 use BackupPC::Xfer::RsyncDigest qw(:all);
@@ -1145,22 +1146,38 @@ sub fileDeltaRxDone
             #
             my $rxOutFile = $fio->{outDirSh}
                             . $fio->{bpc}->fileNameMangle($name);
-            if ( !link($attr->{fullPath}, $rxOutFile) ) {
-                $fio->log("Unable to link $attr->{fullPath} to $rxOutFile");
-               $fio->{stats}{errorCnt}++;
-               $ret = -1;
-            } else {
-               #
-               # Cumulate the stats
-               #
-               $fio->{stats}{TotalFileCnt}++;
-               $fio->{stats}{TotalFileSize} += $fio->{rxSize};
-               $fio->{stats}{ExistFileCnt}++;
-               $fio->{stats}{ExistFileSize} += $fio->{rxSize};
-               $fio->{stats}{ExistFileCompSize} += -s $rxOutFile;
-               $fio->{rxFile}{size} = $fio->{rxSize};
-               $ret = $fio->attribSet($fio->{rxFile});
-           }
+            my($exists, $digest, $origSize, $outSize, $errs)
+                                = BackupPC::PoolWrite::LinkOrCopy(
+                                      $fio->{bpc},
+                                      $attr->{fullPath},
+                                      $attr->{compress},
+                                      $rxOutFile,
+                                      $fio->{xfer}{compress});
+            #
+            # Cumulate the stats
+            #
+            $fio->{stats}{TotalFileCnt}++;
+            $fio->{stats}{TotalFileSize} += $fio->{rxSize};
+            $fio->{stats}{ExistFileCnt}++;
+            $fio->{stats}{ExistFileSize} += $fio->{rxSize};
+            $fio->{stats}{ExistFileCompSize} += -s $rxOutFile;
+            $fio->{rxFile}{size} = $fio->{rxSize};
+            $ret = $fio->attribSet($fio->{rxFile});
+            $fio->log(@$errs) if ( defined($errs) && @$errs );
+
+            if ( !$exists && $outSize > 0 ) {
+                #
+                # the hard link failed, most likely because the target
+                # file has too many links.  We have copied the file
+                # instead, so add this to the new file list.
+                #
+                my $rxOutFileRel = "$fio->{shareM}/"
+                                 . $fio->{bpc}->fileNameMangle($name);
+                $rxOutFileRel =~ s{^/+}{};
+                my $fh = $fio->{newFilesFH};
+                print($fh "$digest $origSize $rxOutFileRel\n")
+                                                if ( defined($fh) );
+            }
         }
     } else {
        my $exist = $fio->processClose($fio->{rxOutFd},
@@ -1285,10 +1302,16 @@ sub fileListEltSend
         size  => $a->{size},
         %$extraAttribs,
     };
+    my $logName = $f->{name};
+    from_to($f->{name}, "utf8", $fio->{clientCharset})
+                            if ( $fio->{clientCharset} ne "" );
     $fList->encode($f);
-    $f->{name} = "$fio->{xfer}{pathHdrDest}/$f->{name}";
-    $f->{name} =~ s{//+}{/}g;
+
+    $logName = "$fio->{xfer}{pathHdrDest}/$f->{name}";
+    $logName =~ s{//+}{/}g;
+    $f->{name} = $logName;
     $fio->logFileAction("restore", $f) if ( $fio->{logLevel} >= 1 );
+
     &$outputFunc($fList->encodeData);
     #
     # Cumulate stats
index bc4f787..4ad5202 100644 (file)
@@ -221,8 +221,13 @@ sub readOutput
             $t->{XferLOG}->write(\"$_\n") if ( $t->{logLevel} >= 2 );
             $t->{fileCnt}++;
         } else {
-            $t->{XferLOG}->write(\"$_\n") if ( $t->{logLevel} >= 0 );
-            $t->{xferErrCnt}++;
+            #
+            # Ignore annoying log message on incremental for tar 1.15.x
+            #
+            if ( !/: file is unchanged; not dumped$/ ) {
+                $t->{XferLOG}->write(\"$_\n") if ( $t->{logLevel} >= 0 );
+                $t->{xferErrCnt}++;
+            }
            #
            # If tar encounters a minor error, it will exit with a non-zero
            # status.  We still consider that ok.  Remember if tar prints
index f53d984..ee365d5 100755 (executable)
--- a/makeDist
+++ b/makeDist
@@ -42,15 +42,17 @@ use Getopt::Std;
 
 umask(0022);
 
-my $Version     = "2.2.0alpha";
-my $ReleaseDate = "15 Aug 2004";
+my $Version     = "3.0.0alpha";
+my $ReleaseDate = "23 Jan 2006";
 my $DistDir     = "dist/BackupPC-$Version";
 
 my @PerlSrc = qw(
     bin/BackupPC
     bin/BackupPC_archive
     bin/BackupPC_archiveHost
+    bin/BackupPC_attribPrint
     bin/BackupPC_dump
+    bin/BackupPC_fixupBackupSummary
     bin/BackupPC_link
     bin/BackupPC_nightly
     bin/BackupPC_restore
@@ -59,6 +61,7 @@ my @PerlSrc = qw(
     bin/BackupPC_trashClean
     bin/BackupPC_tarExtract
     bin/BackupPC_tarCreate
+    bin/BackupPC_tarPCCopy
     bin/BackupPC_compressPool
     bin/BackupPC_zipCreate
     bin/BackupPC_zcat