#!/usr/bin/perl
# $Id: backup.pl,v 1.16 2007/10/09 06:31:38 matteo Exp $
# 
# Procedura di backup generica
# Lo script cerca sullo STDIN i percorsi alle risorse da salvare, ne fa un tar
# compresso gzip e mette l'archivio nella directory passata come argomento
# o, in mancanza di questo, in quella di default.
# E' possibile indicare anche db mysql da salvare, utilizzando la sintassi
# mysql://username:password@host[:port]/database
#
# Requisiti:
# tar (preferibilmente gnu tar per avere l'output su stdout, bsdtar manda l'output su stderr)
# gzip
# mysqldump
# mysql
# curl (se necessario il trasferimento via ftp del backup)
#
# $Author: matteo $
# $Revision: 1.16 $
# $Date: 2007/10/09 06:31:38 $

#use Net::FTP;

my $scriptdir = &dirname($0);

require "${scriptdir}/config.pl";

$logfile = "${logdir}/backup-" . &gettime() . ".log";
$logfile =~ s/\/\//\//g;
my $logging = 0;
my $needupload = 0;
if($logdir) {
	$logging = 1;
}

$mysqlpattern = "^mysql:\\/\\/([a-zA-Z_0-9\\.]+)(:([^:@\\ ]+))?@(.+)(:([0-9]+))?\\/([a-zA-Z_0-9]+|\\*)\$";

eval {
	open(LOG, ">>${logfile}") or die("[ERROR] Unable to write logs to ${logfile}: $!");
};
if($@) {
	print STDERR $@;
	$logging = 0;
} else {
	print "[INFO] " . localtime() . " Writing logs to ${logfile}\n";
}

&init();

my $info = "[INFO] " . localtime() . " Backup started\n";
&log($info);
print $info;

if(@notfound) {
	my $warning = "[WARNING] Unable to find " . @notfound . " resource(s):\n";
	&log($warning);
	print STDERR ${warning};
	foreach ${item} (@notfound) {
		&log("${item}\n");
		print STDERR "\t${item}\n";
	}
}

if(!(@files) && !(@mysqldbs)) {
	print STDERR "[ERROR] Nothing to backup: enter resources on standard input\n";
} else {
	&delcontents($backupdir);

	foreach $item (@files) {
		&backup($item);
	}

	foreach $item (@mysqldbs) {
		&mysqldump($item);
	}

	# eseguo l'upload del backup via ftp
	# solo se e' definito il parametro $ftpdir
	# e se sono stati eseguiti backup (valorizzata variabile $needupload)
	if($ftpdir && $needupload) {
		&upload();
	}

}

$info = "[INFO] " . localtime() . " Backup finished\n";
&log($info);
print $info;

if($logging) {
	close(LOG);
	if(system("$gzip $logfile")) {
		print STDERR "[WARNING] " . localtime() . " Unable to gzip logfile $logfile\n";
	}
}

# elimino il contenuto di una directory
sub delcontents {
	if($#_ < 0) {
		return;
	}
	my $dir = shift;
	opendir(DIR, $dir) or return;
	while($_ = readdir DIR) {
		if(/^\.\.?$/) {
			next;
		}
		$_ = $dir . "/" . $_;
		s/\/\//\//g;
		if(-f) {
			my $message = "[INFO] " . localtime() . " Deleting file $_\n";
			print $message;
			&log($message);
			unlink $_;
		} elsif(-d) {
			&delcontents($_);
			rmdir $_;
		} else {
			my $error = "[WARNING] Unable to remove $_: resource is not a regular file nor a directory\n";
			print STDERR $error;
			&log($error);
		}
	}
	closedir(DIR);
}

# Carico i files di backup sul server via ftp
sub upload {
	my $output = "[INFO] " . localtime() . " Starting ftp upload to ${ftpdir}\n";
	print $output;
	&log($output);
	opendir(DIR, $backupdir);
	@files = grep !/^\.\.?$/, readdir DIR;
	closedir DIR;
	my $filelist = "";

#	my $ftp = Net::FTP->new($ftphost) or {
#		$output = "[ERROR] " . localtime() . " Unable to connect to $ftphost\n";
#		print $output;
#		&log($output);
#		return;
#	}
#	$ftp->login($ftpuser, $ftppass) or {
#		$output = "[ERROR] " . localtime() . " Cannot login to ${ftphost}: " . $ftp->message . "\n";
#		print $output;
#		&log($output);
#		return;
#	}


	foreach $file (@files) {
		$file =~ s/^/${backupdir}\//g;
		$file =~ s/\/\//\//g;
		if($filelist) {
			$filelist .= ",";
		}
		$filelist .= $file;
	}
	if($filelist) {
		my $command = "${curl} -sS --ftp-create-dirs -u ${ftpuser}:${ftppass} -T \"\{${filelist}\}\" ${ftpdir}";
		my $logcommand = "[INFO] " . localtime() . " ${curl} -sS --ftp-create-dirs -u ${ftpuser}:* -T \"\{${filelist}\}\" ${ftpdir}";
		&log("$logcommand\n");
		open(FTP, "${command} |");
		while(<FTP>) {
			print $_;
			&log($_);
		}
		close(FTP);
	}
	$output = "[INFO] " . localtime() . " Upload finished\n";
	print($output);
	&log($output);
}

# subroutine che esegue il backup
# della risorsa passata come argomento
sub backup {
	if($#_ < 0) {
		print STDERR "[ERROR] Backup what? The 'backup' subroutine needs to know what to backup\n";
		return;
	}
	my $resource = shift;
	$resource =~ s/\n//g;
	$resource =~ s/^ *//g;
	$resource =~ s/\/+$//g;
	if(!($resource =~ /^\//)) {
		print STDERR "[ERROR] The resource ${resource} is not an absolute path name\n";
		return;
	}
	if(!(-e $resource)) {
		print STDERR "[ERROR] The resource ${resource} does not exist\n";
		return;
	}

	my $tarname = substr($resource, 1);
	# rimuovo il carattere newline
	$tarname =~ s/\n//g;
	$tarname =~ s/\//-/g;
	$_ = "${backupdir}/${tarname}.tar.gz";
	s/\/\//\//g;
	my $command = "${tar} czvf \"$_\"";
	$command .= " -C '/' \"" . substr($resource, 1) . "\"";

	my $output = "[INFO] " . localtime() . " ${resource}=>$_\n";
	print $output;
	$output .= `${command}`;
	&log($output);
	$needupload = 1;
}

sub mysqldump {
	if($#_ < 0) {
		print STDERR "[ERROR] Dump what? The 'dump' subroutine needs to know what to dump\n";
		return;
	}
	my $resource = shift;
	if($resource =~ /${mysqlpattern}/) {
		my $user = $1;
		my $pass = $3;
		my $host = $4;
		my $port = $6;
		my $db = $7;
#		print "${resource} => user:${user};pass:${pass};host=${host};port=${port};db=${db}\n";
#		return;
		if($db eq "*") {
			# eseguo il dump di tutti i db
			# non uso --all-databases, ma ne faccio uno alla volta per
			# 1) avere dump di db diversi su file diversi
			# 2) loggare di quali db faccio il dump
			my $command = "${mysql} -B -e \"show databases\""
				. " -h ${host}"
				. (($port) ? " -P ${port}" : "")
				. " -u${user}"
				. (($pass) ? " -p${pass}" : "");
			open(MYSQL, "${command} |");
			my $row = 0;
			while(<MYSQL>) {
				# ignoro la prima riga perche' indica il nome della colonna
				# e non un db vero e proprio
				if(++$row > 1) {
					s/\n//g;
					my $string = $resource;
					$string  =~ s/\/\*$/\/$_/g;
					# si richiama ricorsivamente
					# chiedendo il dump di un db
					&mysqldump($string);
				}
			}
			close(MYSQL);
			return;
		}
		my $filename = "${backupdir}/${host}-${db}.sql";
		$filename =~ s/\/\//\//g;
		my $command = "${mysqldump} -c --add-drop-table"
			. " -h ${host}"
			. (($port) ? " -P ${port}" : "")
			. " -u ${user}"
			. (($pass) ? " -p${pass}" : "")
			. " -r \"${filename}\""
			. " --databases ${db}";

		# nascondo la password
		$resource = "mysql://${user}:*\@${host}"
			. (($port) ? ":${port}" : "")
			. "/${db}";
		my $gzipped = 0;
		eval {
			system($command);
			if(!(-e $filename)) {
				die("[ERROR] ${filename} not created\n");
			} elsif(-z $filename) {
				unlink($filename);
				die("[ERROR] Unable to dump $resource\n");
			}
			if($gzip) {
				$gzipped = system("$gzip $filename");
				($gzipped == 0) or die("[WARNING] Unable to gzip ${filename}: $!\n");
			}
		};
		if($@) {
			print STDERR $@;
		} else {
			my $output = "[INFO] " . localtime() . " '${resource}'=>'${filename}"
				. (($gzipped == 0) ? ".gz" : "")
				. "'\n";
			print ${output};
			&log($output);
			$needupload = 1;
		}
	}
}

sub log {
	my $output = shift;
	print LOG $output;
}

sub gettime {
	my ($second, $minute, $hour, $day, $month, $yearOffset) = localtime();
	$year = 1900 + $yearOffset;
	$month = &twodigits(++$month);
	$day = &twodigits($day);
	$hour = &twodigits($hour);
	$minute = &twodigits($minute);
	$second = &twodigits($second);

	return "${year}.${month}.${day}-${hour}.${minute}.${second}";
}

sub twodigits {
	if(!(@_)) {
		return "00";
	} else {
		my $number = "00" . $_[0];
		$number =~ s/^.*(..)$/$1/g;
		return $number;
	}
}

sub dirname {
	my $path = shift;
	$path =~ s/^(\.|(\/[\w\.]+)+)\/([\w\.]+)$/$1/g;
	return $path;
}

sub basename {
	my $path = shift;
	$path =~ s/\/?(\w+)$/$1/g;
	return $path;
}

sub init {
	if($#ARGV > 0) {
		die("Usage: $0 [backup-location] < resource-file\n");
	}

	@files = ();
	@mysqldbs = ();
	@notfound = ();
	if($#ARGV == 0) {
		if(!(-e $ARGV[0])) {
			print STDERR "[WARNING] Directory $ARGV[0] not found: will backup to ${backupdir}\n";
		} elsif(!(-w $ARGV[0])) {
			print STDERR "[WARNING] Directory $ARGV[0] not writable: will backup to ${backupdir}\n";
		} else {
			$backupdir = $ARGV[0];
		}
	}
	while(<>) {
		my $found = 0;
		s/\n//g;
		# ignoro le righe vuote
		if(/^[[:blank:]]*$/) {
			next;
		}
		# ignoro le righe commentate
		if(/^[[:blank:]]*#/) {
			next;
		}
		s/ *(.+) */$1/g; # trim

		if(/^mysql:\/\/(.+)$/) {
			# devo fare il dump del db
			# la riga deve essere nel formato
			# mysql://username:password@host/database
			if(/${mysqlpattern}/) {
				$found = 1;
				push(@mysqldbs, $_);
			}
		} else {
			# si tratta di una risorsa su filesystem
			# posso indicarla per esteso anche con file:///percorso/assoluto/alla/risorsa
			s/^file:\/\/(.+)$/$1/g;
#			s/[^a-zA-Z0-9_\/\-\.]//g; # caratteri consentiti nel percorso della risorsa
			if(-e) {
				$found = 1;
				push(@files, $_);
			}
		}
		if(!($found)) {
			push(@notfound, $_);
		}

	}
}


