#!/usr/bin/perl
##
## pg_hotbackup - Hot Backup Script
##
## $Id: pg_hotbackup,v 1.2 2007/01/08 23:33:24 wagnerch Exp $
##
## Copyright (c) 2007 by Chad Wagner - http://www.postgresqlforums.com/
##
##

use strict;
use FileHandle;
use Getopt::Long;
use DBI;
use POSIX qw(strftime);

$pg_hotbackup::VERSION = '0.0.2';

my ($opt_help) = 0;
my ($opt_username) = $ENV{'PGUSER'} || 'postgres';
my ($opt_password) = 0;
my ($opt_compress) = 9;
my ($opt_datadir) = $ENV{'PGDATA'};
my ($opt_backupdir) = undef;
my ($opt_blocksize) = 8192;
my ($opt_label) = 'pg_hotbackup_' . strftime("%Y%m%d_%H%M%S", localtime(time));
my ($opt_passfile) = $ENV{'PGPASSFILE'} || $ENV{'HOME'} . '/.pgpass';
my ($opt_database) = 'postgres';
my ($opt_command) = 
   'tar -pcf - -C @opt_datadir@ --exclude=./pg_xlog . |' .
   'gzip -@opt_compress@c |';

my ($db) = undef;

MAIN:
{
   my ($password) = $ENV{'PGPASSWORD'};

   GetOptions (
       'label|l=s' => \$opt_label
      ,'backupdir|b=s' => \$opt_backupdir
      ,'datadir|D=s' => \$opt_datadir
      ,'compress|Z=i' => \$opt_compress
      ,'username|U=s' => \$opt_username
      ,'password|W' => \$opt_password
      ,'help|h' => \$opt_help
   ) or exit 1;


   ## Show help
   if ($opt_help)
   {
      print qq{
pg_hotbackup $pg_hotbackup::VERSION
Copyright (c) 2007, Chad Wagner, All rights reserved.

Usage:
    pg_hotbackup [OPTION]...

    Options:

     -l <s>, --label=<s>       Use label <s> for backup file
     -b <s>, --backupdir=<s>   Write backup file to directory <s>
     -D <s>, --datadir=<s>     Use postgresql data directory <s>
     -Z <i>, --compress=<i>    Compress files with compression level <i>
     -U <s>, --username=<s>    Connect as user <s>
     -W, --password            Prompt for password

};
      exit 0;
   }


   ## Set a restrictive umask, only owner can read/write files
   umask (077);

   ## Read the password
   $password = read_pgpass();
   if ($opt_password)
   {
      require Term::ReadKey;
      Term::ReadKey->import();

      print "Password: ";
      ReadMode(2);
      chomp ($password = <STDIN>);
      ReadMode(0);
      print "\n";
   }


   ## Check if they specified a backupdir
   unless ($opt_backupdir)
   {
      printf STDERR "no backup directory specified.\n";
      exit 1;
   }

   ## Check if backup directory exists
   unless (-d $opt_backupdir)
   {
      printf STDERR "Backup directory '%s' does not exist\n", $opt_backupdir;
      exit 1;
   }


   ## Check if they specified a datadir
   unless ($opt_datadir)
   {
      printf STDERR "no database directory specified and environment " .
         "variable PGDATA unset\n";
      exit 1;
   }

   ## Check data dir exists
   unless (-f $opt_datadir. '/postmaster.pid' )
   {
      printf STDERR "PID file '%s' does not exist\nIs server running?\n",
         $opt_datadir . '/postmaster.pid';
      exit 1;
   }


   ## Connect to the postgres database
   $db = DBI->connect('DBI:Pg:dbname=' . $opt_database,
      $opt_username, $password,
      { AutoCommit => 0, RaiseError => 0, PrintError => 0 })
      or die $DBI::errstr;


   ## Perform the hot backup
   hotbackup ();


   ## Bye
   $db->commit();
   $db->disconnect();

   exit 0;
};


## Start the backup
sub pg_start_backup ()
{
   my ($st);

   $st = $db->prepare("
      SELECT pg_start_backup(?)
   ")
      or die $DBI::errstr;

   $st->execute($opt_label)
      or die $DBI::errstr;

   $st->finish();
}


## Stop the backup
sub pg_stop_backup ()
{
   my ($st);

   $st = $db->prepare("
      SELECT pg_stop_backup()
   ")
      or die $DBI::errstr;

   $st->execute()
      or die $DBI::errstr;

   $st->finish();
}


## Do the hot backup
sub hotbackup ()
{
   my ($bfh) = new FileHandle ();
   my ($ofh) = new FileHandle ();
   my ($cmd) = rewrite_command ();
   my ($len, $buff, $backup_file);

   $backup_file = $opt_backupdir . '/' . $opt_label . '.tar.gz';

   ## Flag starting position in the write-ahead log
   pg_start_backup ();


   ## Run the backup command, read the file and flush it out
   $bfh->open($cmd) or die $!;
   $ofh->open('>' . $backup_file) or die $!;

   ## Read/write loop
   while (($len = sysread($bfh, $buff, $opt_blocksize)) > 0)
   {
      if (syswrite($ofh, $buff, $len) != $len)
      {
         printf STDERR "Failed to write %ld bytes to '%s'.\n", $len,
            $backup_file;
         last;
      }
   }

   $bfh->close();
   $ofh->close();


   ## Flag ending position, and force log switch
   pg_stop_backup ();
}


## Read in the .pgpass file, if it exists
sub read_pgpass ()
{
   my ($fh) = new FileHandle();
   my ($password);

   $fh->open($opt_passfile) or return undef;

   while (<$fh>)
   {
      s/[\r\n]+//;

      if (/^\s*([^:]+):([^:]+):([^:]+):([^:]+):([^:]+)\s*$/)
      {
         ## These are hardcoded, assumption is you will always be using
         ## localhost.  Doesn't make sense otherwise, does it?
         ## Doesn't matter anyways, we are using a socket connection.
         if (   ($1 eq "*" || $1 eq "localhost")
             && ($2 eq "*" || $2 eq "5432")
             && ($3 eq "*" || $3 eq $opt_database)
             && ($4 eq "*" || $4 eq $opt_username)
            )
         {
            $password = $5;
            last;
         } 
      }
   }

   $fh->close();
   return $password;
}

## Rewrite command string
sub rewrite_command ()
{
   my ($data) = $opt_command;

   while ($data =~ m/\@([^@]+)\@/g)
   {
      my ($off_end) = pos ($data);
      my ($off_start) = $off_end - length($1) - 2;
      my ($var) = substr ($data, $off_start + 1, $off_end - $off_start - 2);
      my ($val) = eval ('$' . $var);

      ## Check if the variable resolved to a value
      unless (defined $val)
      {
         printf STDERR "Failed to get value for variable '%s'.\n", $var;
         last;
      }

      $data = substr ($data, 0, $off_start)
            . $val
            . substr ($data, $off_end);
   }

   return $data;
}

__END__

=head1 NAME

pg_hotbackup - PostgreSQL Hot Backup

=head1 SYNOPSIS

pg_hotbackup [B<--label>=I<label name>] [B<--backupdir>=I<backup directory>]
[B<--datadir>=I<data directory>] [B<--compress>=I<level>]
[B<--username>=I<user name>] [B<--password>]

pg_hotbackup [B<--help>]

=head1 DESCRIPTION

B<pg_hotbackup> is a script that can be used to automatically generate a
hot backup of a running PostgreSQL database.  You must ensure that you
have WAL archiving enabled, as it is an important factor.

=head1 OPTIONS

=over 4

=item B<-l> I<string>, B<--label>=I<string>

Uses I<string> as a label for the archive file and backup process, this is
automatically defaulted to I<pg_hotbackup_YYYYMMDD_HHMISS>.  Only specify
this option if you wish to override the default.

=item B<-b> I<string>, B<--backupdir>=I<string>

Uses I<string> for the backup directory.

=item B<-D> I<string>, B<--datadir>=I<string>

Uses I<string> for the data directory, if this is not specified then the
environment variable I<PGDATA> must be set or the script will error.

=item B<-Z> I<integer>, B<--compress>=I<integer>

Specify a compression level between 0 and 9, the default is 9.

=item B<-U> I<string>, B<--username>=I<string>

Uses I<string> for the user name, if this is not specified then the
environment variable I<PGUSER> should be set, otherwise it will have to
authenticate using ident.

=item B<-W>, B<--password>

Prompt for password input, if this option is not used then the script will
look at the environment variable I<PGPASSFILE> and read the pgpass, or it
will look at I<~/.pgpass>, or it will use the environment variable
I<PGPASSWORD> to determine the password.

=head1 AUTHOR

Chad Wagner <wagnerch@postgresqlforums.com>

=head1 LICENSE

 Copyright (c) 2007, Chad Wagner <wagnerch@postgresqlforums.com>
 All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
 met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in
      the documentation and/or other materials provided with the
      distribution.
    * Neither the name of PostgreSQL nor the names of its contributors
      may be used to endorse or promote products derived from this
      software without specific prior written permission.

 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.

=cut
