#!/usr/bin/perl
#
# bbackup - a tool for handling full and incremental backups with tar.
# Copyright (C) 1999-2000 Christoph Lorenz <ChLorenz@csi.com>
#
# 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
#
#--------------------------------------------------------------------------
# $Id: bbackup,v 1.2 2000/12/04 10:12:23 chlorenz Exp $
#--------------------------------------------------------------------------
# TODOs: Full tapes have to be ejected automatically

use AppConfig qw(:expand :argcount);
use Pod::Usage;

my $REVISION=do { my @r = (q$Revision: 1.2 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r };
my $VERSION="0.52";
my $COPYRIGHT="(C) 1999-2000 by Christoph Lorenz <ChLorenz\@csi.com>";
my $config = AppConfig->new( { CREATE => 1,
                               GLOBAL => { EXPAND => EXPAND_NONE,
					  ARGCOUNT => ARGCOUNT_ONE}});
my $systemdate=();
my %filesystems=();
my $summary="";
my $success=0;

&Init;
&Status;
if ( $config->type() eq "full" ) {
  $success=&FullBackup;
} else {
  $success=&IncrementalBackup;
}
&Mail($summary);
&Exit($success);       # writes stamp files etc..


##########################################################################
sub Init {
##########################################################################
  # Get global variables
  $config->define("columns=s", { default => $ENV{'COLUMNS'} || 80 });
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  $mon++;
  $year += 1900;
  if ( $mday < "10" ) { $mday = "0".$mday; }
  if ( $mon < "10" ) { $mon = "0".$mon; }
  
  $systemdate="$year-$mon-$mday";

  # Read command line arguments
  $config->define("verbose|v", { ARGCOUNT => ARGCOUNT_NONE });
  $config->define("user|u", { default => 0, ARGCOUNT => ARGCOUNT_NONE });
  $config->define("force", { default => 0 , ARGCOUNT => ARGCOUNT_NONE});
  $config->define("full", { ARGCOUNT => ARGCOUNT_NONE });
  $config->define("type", { DEFAULT => ""});
  $config->define("help|?", { ARGCOUNT => ARGCOUNT_NONE });
  $config->define("man", { ARGCOUNT => ARGCOUNT_NONE });
  $config->define("incremental", { default => 0, ARGCOUNT => ARGCOUNT_NONE});
  $config->define("status|s", { default => 0, ARGCOUNT => ARGCOUNT_NONE});
  $config->define("dummy", { default => 0, ARGCOUNT => ARGCOUNT_NONE});
  $config->define("version", { ARGCOUNT => ARGCOUNT_NONE });
  $config->define("nocheck", { ARGCOUNT => ARGCOUNT_NONE });
  $config->args();

  if ( $config->version() ) {
    &Exit_With_Version; 
  }
  
  if ( $config->help() ) {
    &Exit_With_Help;
  }

  if ( $config->man() ) {
    &Exit_With_Man;
  }

  # Get configuration from rcfile
  $config->define("stampdir=s", { default => "/usr/local/etc/bbackup",
				  EXPAND => EXPAND_ALL} );
  $config->define("tar=s", { default => "/bin/tar",
			     EXPAND => EXPAND_ALL});
  $config->define("mail=s", { default => "/bin/mail",
			      EXPAND => EXPAND_ALL});
  $config->define("admin=s", { default => "root\@localhost",
			       EXPAND => EXPAND_ALL});
  $config->define("tempdir=s", { default => "/tmp",
			         EXPAND => EXPAND_ALL});
  $config->define("incbackups=s");
  $config->define("snapshotprefix=s", 
		  { default => "/usr/local/etc/bbackup/snapshot-",
		    EXPAND => EXPAND_ALL});
  $config->define("fullprebackup=s", { EXPAND => EXPAND_ALL});
  $config->define("fullbackupdest=s", { EXPAND => EXPAND_ENV});
  $config->define("fullpostbackup=s", { EXPAND => EXPAND_ALL});
  $config->define("tapes=s", { EXPAND => EXPAND_ALL});
  $config->define("incprebackup=s", { EXPAND => EXPAND_ALL});
  $config->define("incbackupdest=s", { EXPAND => EXPAND_ENV});
  $config->define("incpostbackup=s", { EXPAND => EXPAND_ALL});
  $config->define("lastincpostbackup=s", { EXPAND => EXPAND_ALL});
  if ( -f "./bbackuprc" ) {
    $config->file("./bbackuprc" );
  } elsif ( -f "$ENV{HOME}/bbackuprc" ) {
    $config->file("$ENV{HOME}/bbackuprc");
  } elsif ( -f "/usr/local/etc/bbackup/bbackuprc" ) {
    $config->file("/usr/local/etc/bbackup/bbackuprc");
  } elsif ( -f "/etc/bbackup/bbackuprc" ) {
    $config->file("/etc/bbackup/bbackuprc");
  } elsif ( -f "$ENV{HOME}/etc/bbackuprc" ) {
    $config->file("$ENV{HOME}/etc/bbackuprc"); 
  } else {
    &ErrMsg("Cannot find a valid bbackuprc anywhere..."); &MyExit(10); 
  }

  my %filesysvars = $config->varlist('[_]');
  for my $key ( sort keys %filesysvars ) {
    ( my $filesys, my $identifier ) = split ("_", $key);
    $filesystems{$filesys}{$identifier} = $config->get($key);
    $filesystems{$filesys}{$identifier} =~ s/\$(\w*)/$filesystems{$filesys}{$1}/ig;
  }
  
  # Read stampfile
  $config->define("lastbackup=s", { default => "" });
  $config->define("lastdumplevel=s", { default => "999" } );
  $config->define("lasttape=s", { default => "0" } );
  $config->define("lastfulllabel=s", { default => ""} );
  $config->define("lastinclabel=s", { default => ""} );
  $config->define("lastfullseek=s", { default => ""} );
  $config->define("lastincseek=s", { default => ""} );
  if ( -f $config->stampdir()."/stamp" ) {
    $config->file($config->stampdir()."/stamp");
  } else {
    &Msg("Stampfile at ".$config->stampdir()."/stamp does not yet exist\n".
	 "No problem. It will be created after a successful backup");
    my $stampdir = $config->stampdir();
    if ( $config->stampdir !~ /\/$/ ) { 
      $config->stampdir($config->stampdir."/");
    }
    my $dir_create = &EnsureDirectory($config->stampdir());
    if ( $dir_create ne "" ) {
      &ErrMsg("Cannot create stamp directory ".$config->stampdir().
	      "\n(".
	      $dir_create.")\nAborting.");
      &MyExit(9);
    } else {
      &ErrMsg("Successfully created stamp directory ".$config->stampdir());
    }
  }


  # Check certain settings
  my $defaulttype = ( $config->lastdumplevel() >= $config->incbackups()) ?
  "full" : "incremental";

  if ( $config->type() eq "" ) { $config->type($defaulttype); } 
  if ( $config->full() ne "0" ) { $config->type("full"); }
  if ( $config->incremental() ne "0" ) { $config->type("incremental"); }
  if ( $config->type !~ /f|i/i ) {
    print "type must be either full or incremental.\n";
    exit 1;      # no need to send mails here
  } else {
    $config->type("full") if ( $config->type() =~ /f/i );
    $config->type("incremental") if ( $config->type() =~ /i/i );
  }

  if ( $config->lastbackup() eq $systemdate ) {
    &Msg("You've already made a backup today");
    if ( $config->force() ) {
      &Msg("But you insist to perform another backup today. OK.\n");
    } else {
      &ErrMsg("Aborting...\n");
      exit 10;  # no need to send mails here
    }
  }

  # Expand destination variables
  for my $key ( sort keys %filesystems ) {
    my $fullbackupdest=$config->fullbackupdest();
    my $incbackupdest=$config->incbackupdest();
    my $device = $config->incbackupdest();
    my $number = $config->lastdumplevel()+1;  
    my $tapenumber = $config->lasttape();
    my $easyfilesys = &easystr($key);
    $fullbackupdest =~ s/\$NUMBER/$tapenumber/i; 
    $fullbackupdest =~ s/\$FILESYSTEM/$easyfilesys/i;
    $fullbackupdest =~ s/\$DATE/$systemdate/i;
    $incbackupdest =~ s/\$NUMBER/$number/i;
    $incbackupdest =~ s/\$FILESYSTEM/$easyfilesys/i;
    $incbackupdest =~ s/\$DATE/$systemdate/i;
    if ($filesystems{$key}{fullcompress} =~ /(yes|on|true|1)/i ) {
      $filesystems{$key}{fullcompress}=1;
      if ( $fullbackupdest !~ /\/dev/i ) {
	# No device file => add a .gz
	$fullbackupdest .= ".gz";
      }
    } else {
      $filesystems{$key}{fullcompress}=0; 
    }
    if ($filesystems{$key}{inccompress} =~ /(yes|on|true|1)/i ) {
      $filesystems{$key}{inccompress}=1;
      if ( $incbackupdest !~ /\/dev/i ) {
	# No device file => add a .gz
	$incbackupdest .= ".gz";
      }
    } else {
      $filesystems{$key}{inccompress}=0; 
    }
    $filesystems{$key}{fullbackupdest}=$fullbackupdest;
    $filesystems{$key}{incbackupdest}=$incbackupdest;
    if ( $filesystems{$key}{mountpoint} eq "" ) {
      $filesystems{$key}{mountpoint}=$key;
    }
  }  
}


#########################################################################
sub Status {
#########################################################################
  if ( $config->status()) {
    print "Last dumplevel/max. dumplevels: ".$config->lastdumplevel()."/".
          $config->incbackups()."\n";
    print "Type of backup : ".$config->type()."\n";
    print "Stamp directory: ".$config->stampdir()."\n";
    print "Stamp file     : ".$config->stampdir()."/stamp\n";
    print "Admin          : ".$config->admin()."\n";
    print "Snapshotprefix : ".$config->snapshotprefix()."\n";
    for my $key ( sort keys %filesystems ) {
      print "Filesystem: $key\n";
      print "\tPreaction: ".$filesystems{$key}{preaction}."\n";
      print "\tMountpoint: ".$filesystems{$key}{mountpoint}."\n";
      print "\tFullbackupdest: ".$filesystems{$key}{fullbackupdest}."\n";
      print "\tIncbackupdest: ".$filesystems{$key}{incbackupdest}."\n";
      print "\tFullcompress: ".$filesystems{$key}{fullcompress}."\n";
      print "\tFullexclude: ".$filesystems{$key}{fullexclude}."\n";
      print "\tInccompress: ".$filesystems{$key}{inccompress}."\n";
      print "\tIncexclude: ".$filesystems{$key}{incexclude}."\n";
      print "\tPostaction: ".$filesystems{$key}{postaction}."\n";
    }
  }
}


#########################################################################
sub Msg {
#########################################################################
  print $_[0]."\n" if ( $config->verbose());
  $summary .= $_[0]."\n";
}


#########################################################################
sub ErrMsg {
#########################################################################
  print "*" x $config->columns();
  print $_[0]."\n";
  print "*" x $config->columns();
  $summary .= $_[0]."\n";
}


#########################################################################
sub FullBackup {
#########################################################################
  my $total_bytes=0;
  my $total_time=0;
  my $retval=0;
  my $lastfullabel = $config->lastfulllabel();
  my $lastfullseek = $config->lastfullseek();
  $config->lastfulllabel("");
  $config->lastfullseek("");
  
  if ( $config->dummy()) {
     &Msg("*** Running in dummy mode; don't write anything anywhere ***");
    }
  &Msg("*** Performing full backup on tape #".$config->lasttape().
       " (of ".$config->tapes().") ***");

  ### Command *before* the backup
  if ( $config->fullprebackup() ne "" ) {
    &Msg("\tRunning initial backup action: \n\t\t\t".$config->fullprebackup());
    system($config->fullprebackup());
    if ( $? >= 256 ) {
      &ErrMsg("Error: command \"".$config->fullprebackup()."\" failed\n($!)");
      &ErrMsg("Aborting...");
      &MyExit(2);
    }
  }

  # Checking, if we would overwrite an old archive, but only, if there
  # was a backup made before
  
  if ( ($lastfulllabel ne "") || ($config->lastinclabel() ne "")) {
    my $archive_to_be_overwritten=
    &GetVolumeInfo($config->fullbackupdest(),"");
    if ( $archive_to_be_overwritten eq "no archive" ) {
      # No uncompressed archive found. Try the compressed one.
      $archive_to_be_overwritten=
      &GetVolumeInfo($config->fullbackupdest(),"-z");
    }
    if ( $archive_to_be_overwritten ne "no archive" ) {
      &Msg("\t\tOld volume found: \"".$archive_to_be_overwritten.
	   "\"");
      if ( ($lastfulllabel =~ /$archive_to_be_overwritten,/) ||
	   ($config->lastinclabel() =~ /$archive_to_be_overwritten,/) ) {
	&Msg("\t\tSince this was the last backup, you made, it wouldn't be");
	&Msg("\t\tuseful to overwrite it.");
	if ( !$config->force() ) {
	  &ErrMsg("Aborting this backup run!");
	  return -1;
	}
	&Msg("\t\t!! But you force me to do it !!");
      }
    } else {
      &Msg("\t\tNo old volume found on tape. That's fine.");
    }
  }

  
  ### The backup process itself  (Error: exit 3)
  for my $filesystem ( keys %filesystems ) {
    next if $filesystem eq "";
    if ( !&CheckTape($filesystems{$filesystem}{fullbackupdest})) {
      &ErrMsg("\tCan't write to ".$filesystems{$filesystem}{fullbackupdest}.
	      " Skipping backup of $filesystem .");
      next;
    }
    &Msg("\t--> Backing up $filesystem");

    # Command before the backup process itself
    if ( $filesystems{$filesystem}{preaction} ne "" ) {
      &Msg("\tRunning previous action: \n\t\t\t".
	   $filesystems{$filesystem}{preaction}."\n");
      system($filesystems{$filesystem}{preaction});
      if ( $? >= 256 ) {
	&ErrMsg("Error: command \"".$filesystems{$filesystem}{preaction}.
		"\" failed\n($!)");
	&ErrMsg("Aborting...");
	next;
      }
    }


    # At this point, we can really start.
    # Creating snapshot directory
    if ( ! $config->dummy()) {
      my @dirs=split("/",$config->snapshotprefix()); pop @dirs;
      my $mydir = join("/",@dirs); 
      my $dir_create = &EnsureDirectory($mydir);
      if ( $dir_create ne "" ) {
	&ErrMsg("Cannot create snapshot directory $mydir".
		"\n(".
		$dir_create.")\nAborting.");
	&MyExit(5);
      }
    }
    # Check, if an old snapshot file exists. If yes, rename it for backup
    if ( ! $config->dummy()) {
      if ( -f $config->snapshotprefix().&easystr($filesystem)) {
	unlink $config->snapshotprefix().&easystr($filesystem).".bak";
	if ( rename $config->snapshotprefix().&easystr($filesystem) ,
	     $config->snapshotprefix().&easystr($filesystem).".bak") {
	  &Msg("\t\tRenamed old snapshot file ".$config->snapshotprefix().
	       &easystr($filesystem));
	} else {
	  ErrMsg("Could not rename old snapshot file ".
		 $config->snapshotprefix().&easystr($filesystem).
		 "\t($!)\nAborting!");
	  &MyExit(6);
	}
      } else {
	&Msg("\t\tNo old snapshot file found. That's OK");
      }
    }
    # Constuct the tar command
    my $excludestring="";
    if ( $filesystems{$filesystem}{fullexclude} ne "" ) {
      my @excludes = split /\s/, $filesystems{$filesystem}{fullexclude};
      foreach my $exclude ( @excludes ) {
	$exclude="./".$exclude if ( $exclude !~ /^\.\// );
	$excludestring.="--exclude=$exclude ";
      }
    }
    my $compress = ($filesystems{$filesystem}{fullcompress}) ? "-z" : "";
    my $command="(unset LC_ALL; unset LANG; ".$config->tar().
    " --totals -c $compress -v -f".
    $filesystems{$filesystem}{fullbackupdest}.
    " --label ".$systemdate."-".&easystr($filesystem).
    " -g ".$config->snapshotprefix().&easystr($filesystem). " ". 
    $excludestring." -l -p -R -C ".
    $filesystems{$filesystem}{mountpoint}." . ".
    " | tee ".$config->tempdir()."/bbackup-".&easystr($filesystem).".log)";
    # Running tar
    my $start_time=time();
    my $output="";
    my $tape_start_position=0;
    if ( ! $config->dummy() ) {
      $tape_start_position = 
        `mt -f $filesystems{$filesystem}{fullbackupdest} tell`;
      ($tape_start_position) = ( $tape_start_position =~ /\d+/g);
      &Msg("\t\tBackup started at ".localtime($start_time).
	   " at $tape_start_position");
      if ( $config->verbose()) {
	print "-" x $config->columns()."\n";
	$output=`$command 3>&1 1>&2 2>&3 3>&-`;
	print "-" x $config->columns()."\n";
      } else {
	$output=`$command 3>&1 1>/dev/null 2>&3 3>&-`;
	sleep 10; # Tape can still write its buffer
      }
    } else {
      $output="Total bytes written: 1";
      &Msg("Would run $command");
      system("true");   # Setting $? and $! correct
    }
    my $status = $?; 
    my $error = $!;
    my $time = ( time() - $start_time ); if ( $time < 1 ) { $time = 1 };
    my ( $bytes_written ) = ( $output =~ /Total bytes written: (\d+)/gs );
    # Statistics and error checking
    if ( ($status == 0) && ( $bytes_written > 0 ) ) {
      my $tape_position = `mt -f $filesystems{$filesystem}{fullbackupdest} tell`;
      ($tape_position) = ( $tape_position =~ /\d+/g);
      $total_bytes += $bytes_written;
      $total_time += $time;
      &Msg("\t\tBackup successfully finished at ".localtime(time()).
	   " at position $tape_position");
      &Msg(sprintf "\t\t%.0f bytes written in %d sec (%.0f bytes/sec)\n",
	   $bytes_written,$time,($bytes_written/$time));
      $config->lastfulllabel($config->lastfulllabel().$systemdate."-".
			     &easystr($filesystem).",");
      $config->lastfullseek($config->lastfullseek().$tape_position.",");
    } else {
      &ErrMsg("Backup failed.\n($error)\nTape status: ");
      &CheckTape($filesystems{$filesystem}{fullbackupdest})
      &ErrMsg("\nAborting!");
      &MyExit(7);
    }

    # Command to be launched after the individual backup process
    if ( $filesystems{$filesystem}{postaction} ne "" ) {
      &Msg("\tRunning final action: \n\t\t\t".
	   $filesystems{$filesystem}{postaction}."\n");
      system($filesystems{$filesystem}{postaction});
      if ( $? >= 256 ) {
	&ErrMsg("Error: command \"".$filesystems{$filesystem}{postaction}.
		"\" failed\n($!)");
	&ErrMsg("Continuing with the next steps...");
      }
    }

  }   // for filesystem

  ### Statistics are nice...
  &Msg("\n\t->Backup statistics:");
  &Msg("\t\tAmount of written data: $total_bytes bytes");
  &Msg("\t\tUsed time             : $total_time seconds");
  if ( $total_time > 0 ) {
    &Msg("\t\tThat's a backup rate of ".int($total_bytes/$total_time).
	 " Bytes/sec");
  } else {
    # Pretty sure, that the backup failed.
    $retval=-1;
  }
  
  ### Command *after* the backup
  if ( $config->fullpostbackup() ne "" ) {
    &Msg("\tRunning final backup action: \n\t\t\t".
	 $config->fullpostbackup()."\n");
    system($config->fullpostbackup());
    if ( $? >= 256 ) {
      &ErrMsg("Error: command \"".$config->fullpostbackup()."\" failed\n($!)".
	      "\nAborting!");
      &MyExit(4);
    }
  }
  $config->lastdumplevel(-1);
  $config->lasttape(($config->lasttape()+1) % $config->tapes());
  return $retval;
}


#########################################################################
sub IncrementalBackup {
#########################################################################
  my $total_bytes=0;
  my $total_time=0;
  my $retval=0;
  my $lastinclabel=$config->lastinclabel();
  my $lastincseek=$config->lastincseek();
  $config->lastinclabel("");
  $config->lastincseek("");
  
  if ( $config->dummy()) {
    &Msg("*** Running in dummy mode; don't write anything anywhere ***");
  }
  &Msg("*** Performing incremental backup #".
       ($config->lastdumplevel()+1)." (of ".$config->incbackups().") ***");

  ### Command *before* the backup
  if ( $config->incprebackup() ne "" ) {
    &Msg("\tRunning initial backup action: \n\t\t\t".$config->incprebackup()."\n");
    system($config->incprebackup());
    if ( $? >= 256 ) {
      &ErrMsg("Error: command \"".$config->incprebackup()."\" failed\n($!)");
      &ErrMsg("Aborting...");
      &MyExit(8);
    }
  }


  # Checking, if we would overwrite an old archive
  if ( ($config->lastfulllabel() ne "") || ($lastinclabel ne "" ) ) {
    my $archive_to_be_overwritten=
    &GetVolumeInfo($config->incbackupdest(),"");
    if ( $archive_to_be_overwritten eq "no archive" ) {
      # No uncompressed archive found. Try the compressed one.
      $archive_to_be_overwritten=
      &GetVolumeInfo($config->incbackupdest(),"-z");
    }
    if ( $archive_to_be_overwritten ne "no archive" ) {
      &Msg("\t\tOld volume found: \"".$archive_to_be_overwritten.
	   "\"");
      if ( ($lastinclabel =~ /$archive_to_be_overwritten,/) ||
	   ($config->lastfulllabel() =~ /$archive_to_be_overwritten,/ )) {
	&Msg("\t\tSince this was the last backup, you made, it wouldn't be");
	&Msg("\t\tuseful to overwrite it.");
	if ( !$config->force() ) {
	  &ErrMsg("Aborting this backup run!");
	  return -1;
	}
	&Msg("\t\t!! But you force me to do it !!");
      }
    } else {
      &Msg("\t\tNo old volume found on tape. That's fine.");
    }
  }

  ### The backup process itself  (Error: exit 3)
  for my $filesystem ( keys %filesystems ) {
    next if $filesystem eq "";
    if ( !&CheckTape($filesystems{$filesystem}{incbackupdest})) {
      &ErrMsg("\tCan't write to ".$filesystems{$filesystem}{incbackupdest}.
	      " Skipping backup of $filesystem .");
      next;
    }
    &Msg("\t--> Backing up $filesystem");
    
    # Command before the backup process itself
    if ( $filesystems{$filesystem}{preaction} ne "" ) {
      &Msg("\t\tRunning previous action: \n\t\t\t".
	   $filesystems{$filesystem}{preaction});
      system($filesystems{$filesystem}{preaction});
      if ( $? >= 256 ) {
	&ErrMsg("Error: command \"".$filesystems{$filesystem}{preaction}.
		"\" failed\n($!)");
	&ErrMsg("Skipping this backup");
	next;
      }
    }
    
    
    # At this point, we can really start.
    # Constuct the tar command
    my $excludestring="";
    if ( $filesystems{$filesystem}{incexclude} ne "" ) {
      my @excludes = split /\s/, $filesystems{$filesystem}{incexclude};
      foreach my $exclude ( @excludes ) {
	$exclude="./".$exclude if ( $exclude !~ /^\.\// );
	$excludestring.="--exclude=$exclude ";
      }
    }
    my $compress = ($filesystems{$filesystem}{inccompress}) ? "-z" : "";    
    my $command="(unset LC_ALL; unset LANG ; ".$config->tar().
    " --totals -c $compress ".
    "-v -f ".$filesystems{$filesystem}{incbackupdest}.
    " --label ".$systemdate."-".&easystr($filesystem).
    " -g ".$config->snapshotprefix().&easystr($filesystem)." ". 
    $excludestring." -l -p -R -C ".
    $filesystems{$filesystem}{mountpoint}." . ".
    "| tee ".$config->tempdir()."/bbackup-".
    &easystr($filesystem).".log)";
    
    # Running tar
    my $start_time=time();
    my $output="";
    my $tape_start_position=0;
    if ( ! $config->dummy() ) {
      $tape_start_position = 
        `mt -f $filesystems{$filesystem}{incbackupdest} tell`;
      ($tape_start_position) = ( $tape_start_position =~ /\d+/g);
      &Msg("\t\tBackup started at ".localtime($start_time). 
	   " at tape position $tape_start_position");
      if ( $config->verbose()) {
	print "-" x $config->columns()."\n";
	$output=`$command 3>&1 1>&2 2>&3 3>&-`;
	print "-" x $config->columns()."\n";
	sleep 10;   # Tape can write its buffers...
      } else {
	$output=`$command 3>&1 1>/dev/null 2>&3 3>&-`;
      }
    } else {
      $output="Total bytes written: ";
      &Msg("Would run $command");
      system("true");   # Setting $? and $! correct
    }
    my $status = $?; 
    my $error = $!;
    my $time = ( time() - $start_time ); if ( $time < 1 ) { $time = 1 };
    my ( $bytes_written ) = ( $output =~ /Total bytes written: (\d+)/gs );
    # Statistics and er/mnt/backup/$NUMBER-$FILESYSTEMror checking
    if ( ($status == 0) && ( $bytes_written > 0 ) ) {
      my $tape_position = `mt -f $filesystems{$filesystem}{incbackupdest} tell`;
      ($tape_position) = ( $tape_position =~ /\d+/g);
      $total_bytes += $bytes_written;
      $total_time += $time;
      &Msg("\t\tBackup successfully finished at ".localtime(time()).
	   " at position ".$tape_position);
      
      &Msg(sprintf "\t\t%.0f bytes written in %d sec (%.0f bytes/sec)\n",
	   $bytes_written,$time,($bytes_written/$time));
      $config->lastinclabel($config->lastinclabel().$systemdate."-".
			     &easystr($filesystem).",");
      $config->lastincseek($config->lastincseek().$tape_position.",");
    } else {
      &ErrMsg("Backup failed.\n($error)\nTape report:");
      &CheckTape($filesystems{$filesystem}{incbackupdest})
      &ErrMsg("\nAborting!");
      &MyExit(9);
    }

    # Command to be launched after the individual backup process
    if ( $filesystems{$filesystem}{postaction} ne "" ) {
      &Msg("\tRunning final action: \n\t\t\t".
	   $filesystems{$filesystem}{postaction}."\n");
      system($filesystems{$filesystem}{postaction});
      if ( $? >= 256 ) {
	&ErrMsg("Error: command \"".$filesystems{$filesystem}{postaction}.
		"\" failed\n($!)");
	&ErrMsg("Going on with next steps...");
      }
    }
    
  }

  ### Statistics are nice...
  &Msg("\n\t->Backup statistics:");
  &Msg("\t\tAmount of written data: $total_bytes bytes");
  &Msg("\t\tUsed time             : $total_time seconds");
  if ( $total_time > 0 ) {
    &Msg("\t\tThat's a backup rate of ".int($total_bytes/$total_time).
	 " Bytes/sec");
  } else {
    # Pretty sure, that teh backup failed.
    $retval=-1;
  }
  
  ### Command *after* the backup
  if ( $config->incpostbackup() ne "" ) {
    &Msg("\tRunning final backup action: \n\t\t\t".
	 $config->incpostbackup()."\n");
    system($config->incpostbackup());
    if ( $? >= 256 ) {
      &ErrMsg("Error: command \"".$config->incpostbackup()."\" failed\n($!)".
	      "\nAborting!");
      &MyExit(4);
    }
  }

  ### Command after the last incremental backup
  if ( ($config->lastincpostbackup() ne "")  &&
       (($config->lastdumplevel()+1) >= $config->incbackups()) ) {
    &Msg("\n\tRunning final action after last incremental backup: \n\t\t\t".
	 $config->lastincpostbackup()."\n");
    system($config->lastincpostbackup());
    if ( $? >= 256 ) {
      &ErrMsg("Error: command \"".$config->lastincpostbackup().
	      "\" failed\n($!)\nAborting!");
      &MyExit(4);
    }
  } else {
    &Msg("\n\tThis was backup ".($config->lastdumplevel()+1).
	 " of ".$config->incbackups()."\n");
  }
  
  return $retval;
}


#########################################################################
sub GetVolumeInfo {
#########################################################################
  my $dest = $_[0];
  my $compress = $_[1];
  my $dev_device="";
  my $volume="";

  ( $dev_device ) = ( $dest =~ /^\/dev\/(.*)/i );
  
  if ( ($dev_device ne "") || ( $config->nocheck()) ) {   
    if ( $dev_device !~ /^n/ ) {
      $dev_device="n".$dev_device;
    }
    if ( &CheckTape("/dev/$dev_device")) {
      my $get_volume="dd if=/dev/$dev_device bs=128 count=1 2>/dev/null | ".
      $config->tar()." $compress -tf - | head -1 | cut -d\" \" -f1 2>/dev/null > /dev/null";
      $volume=`$get_volume`;
      system("mt -f /dev/$dev_device bsfm 1 2>/dev/null");
      chomp($volume);
    }
    if ( $volume eq "" ) { $volume="no archive"; }
    return $volume;
  } else {
    # Maybe, it's a plain file, or we don't want to check.
    # Return a "good" message...
    return "no valid archive";
  }
}

#####################################################################
sub Exit {
#####################################################################
  my $success=$_[0];
  if ( $config->lastfulllabel() eq "" ) {
    $config->lastfulllabel("NoValidLabelThatsWeird");
  }
  if ( $config->lastinclabel() eq "" ) {
    $config->lastinclabel("NoValidLabelThatsWeird");
  }
  $config->lastbackup($systemdate);
  $config->lastdumplevel($config->lastdumplevel()+1);
  if ( (!$config->dummy()) && ($success != -1)) {
    # Check, if the next backup will be a full backup
    if ( $config->lastdumplevel() >= $config->incbackups()) {
      # Yes, it is. Find out, which tape will be used next.
      $config->lasttape($config->lasttape()+1);
      if ($config->lasttape() > $config->tapes()) {
	$config->lasttape(1);
      }
      # Yes, it is. Write a mail to the admin
      open(MAIL,"| ".$config->mail()." -s \"BBACKUP NEEDS A TAPE\" ".
	   $config->admin())
      || &ErrMsg("Cannot send mail to ".$config->admin().
		 "! Fix this soon!\n");
      print MAIL "Please insert tape #".$config->lasttape()."\n";
      close MAIL;
    } 
    open(STAMPFILE,">".$config->stampdir()."/stamp") || 
    &ErrMsg("Cannot write stampfile ".$config->stampdir().
	    "/stamp because of $!\n");
    print STAMPFILE "LASTBACKUP=".$config->lastbackup()."\n";
    print STAMPFILE "LASTDUMPLEVEL=".$config->lastdumplevel()."\n";
    print STAMPFILE "LASTTAPE=".$config->lasttape()."\n";
    print STAMPFILE "LASTFULLLABEL=".$config->lastfulllabel()."\n";
    print STAMPFILE "LASTINCLABEL=".$config->lastinclabel()."\n";
    print STAMPFILE "LASTFULLSEEK=".$config->lastfullseek()."\n";
    print STAMPFILE "LASTINCSEEK=".$config->lastincseek()."\n";
    close(STAMPFILE);
    &Msg("Updated stampfile\n");
  }
}


#####################################################################
sub EnsureDirectory {
#####################################################################
  my ($dir) = @_;
  my $failure = "";
  
  my @destdirs = split ( /\//,$dir);  
  # if the last character of $destination is not a "/", it denotes the
  # prefix of the filenames. So, we have to skip this
  if ( $dir !~ /\/$/ ) {
    pop @destdirs;
  }
  
  my $destdir="";
  foreach $dest ( @destdirs ) {
    $destdir .= "/".$dest;
    if ( !-d $destdir ) {
      if ( mkdir $destdir,0777) {
        &Msg("\t\tCreated directory $destdir");
      } else {
	$failure = $!;
        &Msg("\t\tCannot create $destdir as directory ($!)");
      }
    }
  }
  return $failure;
}


#####################################################################
sub easystr {
#####################################################################
  my $string = $_[0];
  
  $string =~ s/\//\#/ig;
  return $string;
}


#####################################################################
sub Exit_With_Help {
#####################################################################
  print "This is bbackup version $VERSION, $COPYRIGHT\n\n";
  pod2usage(1);
  exit;
}


#####################################################################
sub Exit_With_Man {
#####################################################################
  pod2usage(VERBOSE => 2);
  exit;
}


#####################################################################
sub Exit_With_Version {
#####################################################################
  print "This is bbackup version $VERSION, $COPYRIGHT\n\n";
  exit;
}


#####################################################################
sub MyExit {
#####################################################################
  my $exitcode=$_;

  &Mail($summary);
  &Exit(-1);   # since there is an error 
  exit $exitcode;
}


#####################################################################
sub Mail {
#####################################################################
  if ( !$config->dummy() ) {
    open(MAIL, "| ".$config->mail()." -s \"BBACKUP summary of $systemdate\" ".$config->admin());
    print MAIL $summary;
    close(MAIL);
  }
}


#####################################################################
sub CheckTape { 
#####################################################################
  my $device=$_[0];
  my $error=0;

  if ( !$config->nocheck() && ( $device =~ /^\/dev/i)) {
    #system("dd if=$device bs=1 count=1> /dev/null 2>/dev/null");
    #$summary .= "Tape error (dd if=$device bs=1 count=1> /dev/null 2>/dev/null) $!\n";
    my $status = `mt -f $device status`;
    my ($statuscode) = ( $status =~ /General status bits on \((.*?)\)/ );
    #$statuscode="0x"."0" x (8-length($statuscode)). $statuscode;
    #$statuscode="0x".$statuscode;
    #$statuscode=unpack("l", $statuscode);
    $statuscode=eval("0x$statuscode");
    my $IM_REP_EN = ( ($statuscode & 0x00010000) == 0x00010000 );
    my $DR_OPEN = ( ($statuscode & 0x00040000) == 0x00040000 );
    my $GMT_ONLINE = ( ($statuscode & 0x01000000) == 0x01000000);
    my $GMT_WR_PROT = ( ($statuscode & 0x04000000) == 0x04000000);
    my $GMT_EOD = ( ($statuscode & 0x08000000) == 0x08000000);
    my $GMT_EOT = ( ($statuscode & 0x20000000) == 0x20000000);
    my $GMT_BOT = ( ($statuscode & 0x40000000) == 0x40000000);
    my $GMT_EOF = ( ($statuscode & 0x80000000) == 0x80000000);
    $error =  ( $DR_OPEN || $GMT_EOD || $GMT_EOT || $GMT_WR_PROT );
    if ( $error ) {
      my $errormsg= "Tape error ($statuscode): ";
      $errormsg .= "tape is full (at the end of the file); " if ( $GMT_EOF );
      $errormsg .= "no tape in drive; " if ( $DR_OPEN );
      $errormsg .= "tape is write protected; " if ( $GMT_WR_PROT );
      $errormsg .= "tape is full (at the end of tape); " if ( $GMT_EOT );
      $errormsg .= "tape is full (at the end of device); " if ( $GMT_EOD );
      &ErrMsg($errormsg);
      $summary .= $errormsg."\n";
    }
    return (!$error);
  } else { return 1; }
}


__END__


################ Documentation ################

=head1 NAME

bbackup - a backup program

=head1 SYNOPSIS

bbackup [options]

Options:

  -dummy                      do not write anything
  -force                      force makeing a backup
  -full                       force backup to be a full backup
  -help                       short documentation
  -incremental                force backup to be an incremental backup
  -man                        full documentation
  -nocheck                    Skip all tape checks
  -status                     show all internal variables
  -user                       <not yet implemented>
  -verbose                    be verbose
  -version                    show version number

=head1 OPTIONS

=over 8

=item B<-dummy>

Pretends to run a backup. But doesn't change the stamp file and doesn't
even call tar. Hence doesn't change the snapshot files, too

=item B<-force>

Perform a backup, even, if there was already a backup made today. 
This overrides the default, that backups cannot be made more than
once a day.

=item B<-full>

Force bbackup to make a full backup even, if the backup was supposed to
be an incremental backup

=item B<-help>

Show a help summary and exit.

=item B<-incremental>

Forces bbackup to perform the current backup as an incremental, 
even if the current backup would be a full one. If the latter is
the case, then the next backup will be a full one, unless you
force bbackup to make another incremental backup.

=item B<-nocheck>
Skip all tape checks. Assume, that everything is OK (tape inserted
and accessible, no valid contents on tape, so overwriting is not
dangerous).

=item B<-man>

Displays this man page and exit.

=item B<-status>

Dumps the status of some internal variables. This options is
only useful for developers of bbackup, but not for normal users.

=item B<-verbose>

Be verbose. Tell the user about everything, bbackup does. Prints
also the whole output of the tar commands.
This options is not useful, if you can bbackup via cron.

=item B<-version>

Show version information and exit.

=back

=head1 DESCRIPTION

bbackup is a sophisticated frontend for GNU-tar. It allows to
perform full backups as well, as incremental backups. It allows
to handle a number of filesystems, to be backed up, each with
their own parameters (see later). It is possible to write
backups either to streaming media or to plain files (perferrably
on an own harddisk).

bbackup uses its own configuration file, which can be in one of
the following positions (the first found one will be used):

  - ./bbackuprc
  - ~/bbackuprc
  - /usr/local/etc/bbackup/bbackuprc
  - /etc/bbackup/bbackuprc
  - ~/etc/bbackuprc

=head1 BBACKUPRC

The file bbackuprc consists of two parts:

1.) Variable definions a la key=value
2.) Blocks, denoted by [blockname] with key=value definitions inside.

Please look at the sample bbackuprc.

=head2 BBACKUPRC - part one

B<stampdir> This tells bbackup, in which directory to store its internal data.
Default: /usr/local/etc/bbackup

B<tar> Location of tar. Default: /bin/tar

B<mail> Location of the mail program. Default: /bin/mail .
The mail program is called the following way:
C<echo "Mail body" | ${mail} -s "subject of the mail" user@host>

B<admin> Mail adress of the person, who receive status mails of bbackup.
Default: root@localhost

B<tempdir> Directory, where temporary files (which can be quite large) like
the verbose tar output are written to.
Default: /tmp

B<incbackups> Number of incremental backups, before a full backup
is written. MUST BE SET!

B<shapshotprefix> Directory and file name prefix, for the tar snapshot files 
for the incremental backup runs. 
Default: C</usr/local/etc/bbackup/snapshot->

B<tapes> Number of available tapes.

B<fullbackupdest> Destination or file, where the full backup goes to.
You can use the following (automatically) predefined variables:

C<$DATE>      : The current date, written in YYYY-MM-YY
C<$NUMBER>    : The serial number of the current incremental backup
C<$FILESYSTEM>: The name of the filesystem to be backed up.
                (Note: All / are replaced by #)

B<incbackupdest> Destination or file, where the incremental backup
goes to. The following variables are predefined:

C<$DATE>      : The current date, written in YYYY-MM-YY
C<$NUMBER>    : The serial number of the used tape
C<$FILESYSTEM>: The name of the filesystem to be backed up.
                (Note: All / are replaced by #)

B<fullprebackup> Action (shell action) before the full backup is started

B<fullpostbackup> Action (shell action) after the last full backup
was made. You can for example rewind your tape and put it offline
with a mt command (C<mt -f /dev/nst0 rewoffl>)

B<incprebackup> same as B<fullprebackup>, but for incremental backups

B<incpostbackup> same as B<fullpostbackup>, but for incremental backups

B<lastincpostbackuu> same as B<fullpostbackup>, with the exception,
that this command is executed after the last incremental backup. 
It's a good idea to use this command to rewind your tape, which holds
the incremental backups, and to put it offline afterwards. The
example command would be C<mt -f /dev/nst0 rewoffl> here.

All variables, except B<fullbackupdest> and B<incbackupdest> can contain
variables ($variable) or even environment variables (${variable}) which
will be expanded at runtime. B<fullbackupdest> and B<incbackupdest> can
contain only environment variables.

=head2 BBACKUPRC part two

Here are all filesystems, which have to be stored. They can be either
local or can be imported at run time.

Each filesystem is identified by a [identifier] line. 
(Example: [/home] ) You don't need to specify the mount point; you
can use any name here (Example: [workmachine:home]), but in this case,
you must set the B<mountpoint> to the mount point, where that certain
directory is mounted to at run time.

Now, the following variables may be defined. They are all optional (with
the exception of the B<mountpoint> variable, if you don't use the
mount point as identifier)

B<preaction> (Shell) action to be run before the file system is
accessed. Examples are here mounting foreign file systems via
NFS or Samba on your main backup server

B<postaction> (Shell) action to be run after backing up the filesystem.

B<mountpoint> Actual mount point of the filesystem. Only to be
specified, if the the [....] identifier does not contain the
currently valid mount point of the filesystem.

B<fullcompress=yes> If you want to compress your full backup. BAD idea for
full backups !

B<inccompress=yes> If you want to compress your incremental backup

B<fullexclude> Pattern for all files to be excluded in a full
backup. Entries are seperated by whitespaces. BAD idea for full backups!
(Example: * /tmp/useless* ).
Since bbackup does not traverse filesystems and therfor stays local
in the filesystem, all excludes must start with a C<./> at the beginning.
But to stop confusing the users, bbackup automatically puts a C<./> at
the beginning of each exclude object.

B<incexclude> Same as B<fullexclude>, but for incremental backups.


All variables of this block can import values from other variables of
the same block and filesystem by calling them via $variable.
(See the example files)

=head1 BUGS

bbackup is not tested long enough

bbackup complains on fresh tapes (tells about tar and dd errors). Can
be ignored.

=head1 AUTHOR

Christoph Lorenz <ChLorenz@csi.com>
