#!/usr/bin/perl
#
# Copyright (c) 2006 Zmanda Inc.  All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 as published
# by the Free Software Foundation.
#
# 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
#
# Contact information: Zmanda Inc, 505 N Mathlida Ave, Suite 120
# Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
#
#

use strict;
use warnings;
use File::Spec::Functions;
use POSIX qw(strftime);
use File::Temp qw/ :POSIX /;
use lib "/usr/local/lib/mysql-zrm";
use ZRM::Common;
use File::Path;

my $MYSQL_BIN_PATH;
my $MYSQLBINLOG="mysqlbinlog";

my $showDetails = -1;
my $outputFormat = "html";
my $parsedString="";
my $POS = "";

my $BEGIN_ROW = "<tr>";
my $END_ROW = "</td></tr>";
my $BEGIN_CELL = "<td>";
my $END_CELL = "</td>";
my $BEGIN_TABLE = "<table border=1>";
my $END_TABLE = "</table>";
my $BEGIN_HEADER = "";
my $END_HEADER = "";
my $BR = "<br>";

#usage strings
my $USAGE_PARSE_BINLOGS_STRING=
		"\t\t[--source-directory <directory name>]\n".
		"\t\t[--bin-logs <\"/fullpath/name1\ /fullpath/name2\ ...\">]\n".
		"\t\t[--row-event-details=never|always|auto]\n".
		"\t\t[--output-format=html|text]\n".
		"\t\t[--mysql-binpath <mysql binaries directory>]\n".
		"\t\t[--parse-binlogs-plugin <plugin>]\n".
		"\t\t[--parse-binlogs-plugin-options <\"option1 option2...\">]\n";

my @PARSEOPT=qw/
		source-directory=s
		bin-logs=s
		output-format=s
		row-event-details=s
		mysql-binpath=s
		parse-binlogs-plugin=s
		parse-binlogs-plugin-options=s
	       /;

#displays $pstring
sub displayParsedString()
{
	if( $parsedString ){
		my $str = $parsedString;
		my $r = &execPlugin( "parse-binlogs-plugin", $str );
		if( $r == 0 ){
			print "$parsedString $END_ROW\n";
		}else{
			if( $verbose ){
				&printLog( "Will not display line $parsedString\n" );
			}
		}
		$parsedString = "";
	}
}

# Takes a string of the form that mysqlbin spits out and
# returns a properly formated timestamp
# $_[0] the string to format.
sub getTimeString()
{
        my $x = $_[0];
        my @a = split( / /, $x );
        my $l = @a;
        my $date = $a[0];
        my $time = $a[$l-1];
        my $y = substr( $date, 0, 2 );
        my $m = substr( $date, 2, 2 );
        $m -= 1;
        my $d = substr( $date, 4, 2 );
        my @t = split( /:/, $time );
        return strftime( "%y-%m-%d %H:%M:%S", $t[2], $t[1], $t[0], $d, $m, $y );
}

# $_[0] line to convert
# return converted line
# It removes \n and 
# converts 
#	< to &lt; 
#	> to &gt;
#	& to &amp;
#	' to &#039;
#	" to &quot;
sub convertToHTML()
{
	my $l = $_[0];
	$l =~ s/\n//g;	
	if( $outputFormat ne "text" ){
		$l =~ s/&/&amp;/g;
		$l =~ s/>/&gt;/g;
		$l =~ s/</&lt;/g;
		$l =~ s/'/&#039;/g;
		$l =~ s/"/&quot;/g;
	}
	return $l;
}

# $_[0] name of bin log file
# $_[1] one line in the bin log file
# the $pstring = ""; statements are there to ensure that people reading
# the code understand what statements are being ignored
sub processOneLine()
{
	my $file = $_[0];
	my $line = $_[1];
	chomp( $line );
	my $orgLine = $line;
	my $pstring="";
	if( $line=~/^# at/ ){
		my $x = $';
		$x=&removeBlanks($x);
		if( $POS eq "" ){
			$POS = $x;
		}
	}elsif( $line=~/^#\d\d\d/ ){
		&displayParsedString();
		$file = &convertToHTML( $file );
		$pstring = "$BEGIN_ROW $BEGIN_CELL $file $END_CELL $BEGIN_CELL $POS $END_CELL $BEGIN_CELL ";
		$line = substr( $line, 1, -1 );
		my @sec = split( /server/, $line );
		$sec[0] = &removeBlanks( $sec[0] );
		my $timestamp = &getTimeString( $sec[0] );
		my @sec1 = split( /\t/, $orgLine );
		if( $sec1[0] =~ /end_log_pos\s+(\d+)\s+$/ ){
			$POS = $1;	
		}
		my $la = @sec1;
		$pstring .= "$timestamp $END_CELL $BEGIN_CELL ";
		if( $la gt 1 ){
			$sec1[1] = &convertToHTML( $sec1[1] );
			$pstring .= $sec1[1];
		}
		$pstring .= " $END_CELL $BEGIN_CELL ";
	}elsif( $line=~/^###/ ){
		my $l = &convertToHTML( $' );
		$pstring = $pstring." $l $BR";
	}elsif( $line=~/^#/ ){
		#ignores all other comments;
		$pstring = "";
	}elsif( $line=~/^\/\*\!\d*\s*SET / ){
		#ignores all lines starting with /*!<version> SET
		#other statements starting with /*! is printed
		#$pstring = "";
	}elsif( $line=~/^DELIMITER ;/ ){
		#ignores all of the DELIMITER directives
	}elsif( $line=~/^DELIMITER \/\*\!\*\/;/ ){
		#ignores all of the DELIMITER directives
	}elsif( $line=~/^SET / ){
		#ignores all of the SET directives
		$pstring = "";
	}elsif( $line=~/ROLLBACK \/\* added by mysqlbinlog \*\/\;/ ){
		#ignores all the ROLLBACK directive lines
		$pstring = "";
	}else{
		if( $parsedString ne "" ){
			my $l = &convertToHTML( $line );
			$pstring = $pstring."$l $BR";
		}
	}
	if( $pstring ){
		$parsedString .= $pstring;
	}
}

#Adds the mysql related parameters from the command line to the mysql command
#$_[0] specifies the mysql command
sub addBinPath()
{
	my $comm = $_[0];
        if( $MYSQL_BIN_PATH ){
        	$comm = catfile( "$MYSQL_BIN_PATH", $comm );
        }
        return $comm;
}

sub setShowDetailsFlag()
{
	if( !defined $inputs{"row-event-details"} ){
		$inputs{"row-event-details"} = "auto";
	}
	if( $inputs{"row-event-details"} eq "never" ){
		$showDetails = 0;
	}elsif( $inputs{"row-event-details"} eq "always" ){
		$showDetails = 1;
	}elsif( $inputs{"row-event-details"} eq "auto" ){
        	my $x = &addBinPath( $MYSQLBINLOG );
		$x = "\"$x\"";
		$x .= " --help";
		my $s = &execCmdAndGetOutput( $x );	
		if( ! defined $s ){
			&printAndDie( "Error executing '$x'\n" );
		}
		if( $s =~ /\n\s+\-v,\s\-\-verbose\s+/ ){
			$showDetails = 1;
		}else{
			$showDetails = 0;
		}
	}else{
		&printAndDie( "Unknown option ".$inputs{"row-event-details"}."\n" );
	}

	if( $verbose ){
		my $msg = "Row Details will ";
		if( $showDetails == 0 ){
			$msg .= "not ";
		}
		$msg .= "be shown\n";
		&printLog( $msg );
	}
}

#$_[0] bin log file to parse
sub parseOneBinLog()
{
        my $binfile = $_[0];
	my $file = $binfile;
        if( ! -f $binfile ){
                &printError( "Cannot find bin log file $_[0] \n" );
                return;
        }
	$binfile = "\"$binfile\"";
        my $x = &addBinPath($MYSQLBINLOG);
	$x = "\"$x\"";
	my $v = "";
	if( $showDetails == -1 ){
		&setShowDetailsFlag();
	}
	if( $showDetails == 1 ){
		$v = "-v -v --base64-output=DECODE-ROWS";	
	}
        $x = $x." $v ".$binfile;
        if( $verbose ){
                &printLog( "Parsing binlog $binfile using command $x\n" );
        }

        unless( open( TMPFILE, "$x|" ) ){
                &printError( "mysqlbinlog failed for file $binfile\n" );
                return;
        }

	$POS = "";
        while( <TMPFILE> ){
                &processOneLine( $file, $_ );
        }
        close TMPFILE;
}
sub printHeader()
{
	print "$BEGIN_TABLE\n";
	print "$BEGIN_HEADER";
	print "$BEGIN_ROW ";
	print "$BEGIN_CELL Log filename $END_CELL";
	print "$BEGIN_CELL Log Position $END_CELL";
	print "$BEGIN_CELL Timestamp $END_CELL";
	print "$BEGIN_CELL Event Type $END_CELL";
	print "$BEGIN_CELL Event ";
	print "$END_ROW\n";
	print "$END_HEADER";
}
sub printTrailer()
{
	print "$END_TABLE\n";
}

#$_[0] list of bin log files to be parsed
sub parseBinLogFiles()
{
	my @a = glob $_[0];
	&printHeader();
	foreach( @a ){
		&parseOneBinLog( $_ );
	}
	&displayParsedString();
	&printTrailer();
}


# windows using the windoes client and then call mysqlbinlog on it.
# $_[0] is the source directory
sub extractBinLogs()
{

        my $a = catfile ( $_[0], "ZRM_BINLOGS" );

        opendir(DIR, "$a") or die "Could not open dir $a\n";
        my @files = readdir(DIR);
        my $i ;
        my $temp;
        my $tmpDir =  catfile ( $a, "temp" );
        mkpath( $tmpDir );
        foreach $i (@files)
        {
                if ( $i =~ /.zip/ ) {
                        my $fn = $`;
                        my $zf = catfile( $a, $i );
                        my $th = $inputs{"host"};
                        $inputs{"host"} = "localhost";
                        my $r = &copyTo( $zf, $tmpDir, $inputs{"host"} );
                        $inputs{"host"} = $th;
                        if( $r == 0  ) {
                                closedir(DIR);
                                rmtree $tmpDir;
                                return $r;
                        }
                }
        }
        closedir(DIR);
        return 0;

}


# $_[0] is the source directory
sub getBinLogFiles()
{

        my $file = $_[1];
        my $x = $_[2];

        my $a = catfile ( $_[0], "ZRM_BINLOGS", "temp" );

        opendir(DIR, "$a") or die "Could not open dir $a\n";
        my @files = readdir(DIR);

        my $i ;
        my $temp;
	my $result;
	&printHeader();
        foreach $i (@files)
        {
		if ( $i eq "." || $i eq ".." ) {
			next;
		}
		my $zf = catfile( $a, $i );
		&parseOneBinLog( $zf );
        }
	&displayParsedString();
	&printTrailer();
        closedir(DIR);
        return $a;
}

sub doParseBinLogs()
{
	my $r;
	if ( $onWindows && $inputs{"bin-logs"} ) {
		&printAndDie( "bin-logs option unsupported for windows server\n" );

	}
	if( $inputs{"source-directory"} ) {
		if( $inputs{"bin-logs"} ){
			&usage( "Both --source-directory and --bin-logs cannot be specified at the same time. Please supply only one of these.\n" );
		}
		if( !-d $inputs{"source-directory"} ) {
			&printAndDie( "Cannot find source directory ".$inputs{"source-directory"}."\n" );
		}
		my $filename = catfile( $inputs{"source-directory"}, $INDEX_FILENAME );
		$r = &parseIndexFile( $filename );
		if( $r == 0 ){
			&printAndDie("cannot open index file $filename $!\n");
		}
		if(  ! ($onWindows) && (defined $indexdata{"compress"} || defined $indexdata{"encrypt"}) ){
			$r = 1;
			$r = &uncompressBackup( $inputs{"source-directory"} );
			if( $r == 0 ){
				&printAndDie( "Unable to uncompress backup\n" );
			}
		} 
		if  ( $onWindows ) {
			my $d = $inputs{"source-directory"};
			my $tmpAction = $action;
			$action = "restore";
			$r = &extractBinLogs( $d );
			$action = $tmpAction;
			if ( $r != 0 ) {
				&printAndDie( "Error in extracting binary logs\n");
			}
		}
	}else{
		if( !$inputs{"bin-logs"} ){
			&usage( "For parse-binlogs action specify either --source-directory or --bin-logs\n" );
		}
	}
	if( $inputs{"source-directory"} && $indexdata{"backup-level"} == 0 ){
		&printAndDie( $inputs{"source-directory"}." contains a full backup and there are no binary logs in this directory.\n" );
	}

        if( $inputs{"mysql-binpath"} ){
                if( ! -d $inputs{"mysql-binpath"} ){
                        &printAndDie( "mysql-binpath not found ".$inputs{"mysql-binpath"}."\n" );
                }
                $MYSQL_BIN_PATH=$inputs{"mysql-binpath"};
        }

	if( defined $inputs{"output-format"} ){
		$outputFormat = $inputs{"output-format"};
		if( $outputFormat ne "html" && $outputFormat ne "text" ){
			&printAndDie( "Unknown output-format. Valid values for output-format is either html or text\n" );
		}
	}else{
		$outputFormat = "html";
	}

	if( $outputFormat eq "text" ){
		$BR = "; ";
		$BEGIN_TABLE = "  ------------------------------------------------------------";
		$END_TABLE = "  ------------------------------------------------------------";
		$END_HEADER = "  ------------------------------------------------------------\n";
		$BEGIN_ROW = "";
		$END_ROW = "";
		$BEGIN_CELL = "";
		$END_CELL = "|";
	}

	my $res="";
	if( $inputs{"bin-logs"} ){
		$res = $inputs{"bin-logs"};
	}else{
		my $t = $inputs{"source-directory"};
		if( $t=~/\s/ ){
                	$t = "\"$t\"";
        	}
		if ( $onWindows ) {
			$res = &getBinLogFiles( $inputs{"source-directory"} );
		} else {
			$res = catfile( $t, $indexdata{"incremental"} );
			&parseBinLogFiles( $res );
		}
	}
	if( ( !$onWindows ) &&  defined $indexdata{"compress"} ){

		&removeUncompressedBackup( $inputs{"source-directory"}, \%indexdata );
	}
	if ( $onWindows ) {
		rmtree($res);
	}
}

#Sets up defaults for backup
sub setupDefaults()
{
        $action = "parse-binlogs";
        $USAGE_STRING = $USAGE_PARSE_BINLOGS_STRING.$USAGE_COMMON_OPTIONS_STRING;
}

sub main()
{
        &setupDefaults();
        &initCommon(@PARSEOPT);
	&createConfigFile();
        &doParseBinLogs();
        &my_exit();
}

&main();
