#!/usr/bin/perl -w
#
# psinib - Perl Snapshot Is Not Incremental Backup
#
# written by Dobrica Pavlinusic <dpavlin@rot13.org> 2003-01-03
# released under GPL v2 or later.
# 
# Backup SMB directories using file produced by LinNeighbourhood (or some
# other program [vi :-)] which produces file in format:
#
# smbmount service mountpoint options
#
#
# usage:
# 	$ psinib.pl mountscript

use strict 'vars';
use Data::Dumper;
use Net::Ping;
use POSIX qw(strftime);
use List::Compare 0.30;
use Filesys::SmbClient;
#use Taint;
use Fcntl qw(LOCK_EX LOCK_NB);
use Digest::MD5;
use File::Basename;
use Getopt::Long;
use File::Path;
use Pod::Usage;
use Filesys::Df;

my $VERSION = '1.0-rc4';

# configuration
my $LOG_TIME_FMT = '%Y-%m-%d %H:%M:%S';	# strftime format for logfile
my $DIR_TIME_FMT = '%Y%m%d';		# strftime format for backup dir

# define timeout for ping
my $PING_TIMEOUT = 5;

my $LOG = '/var/log/backup.log';	# add path here...
#$LOG = '/tmp/backup.log';

# store backups in which directory
my $BACKUP_DEST = '/backup/';

# files to ignore in backup
my @ignore = ('.md5sum', '.backupignore', 'backupignore.txt');

my %ip_cache;

# open log
open(my $log_fh, '>>', $LOG) or die "can't open log $LOG: $!";
select((select($log_fh), $|=1)[0]);	# flush output

# dump warn and dies into log
$SIG{'__WARN__'} = sub { xlog('WARN',$_[0],1) ; exit 1 };
$SIG{'__DIE__'} = sub { xlog('DIE',$_[0],0) ; exit 1 };

# make a lock on logfile

my $c = 0;
{
	flock $log_fh, LOCK_EX | LOCK_NB and last;
	sleep 1;
#	warn "waiting for lock\n";
	redo if ++$c < 10;
	# no response for 10 sec, bail out
	xlog("ABORT","can't take lock on $LOG -- another $0 running?");
	exit 1;
}

# taint path: nmblookup should be there!
$ENV{'PATH'} = "/usr/bin:/bin";

my $use_ping = 1;	# default: use syn tcp ping to verify that host is up
my $verbose = 1;	# default verbosity level
my $quiet = 0;
my $diff = 0;
my $email;
my $max_share_size;	# don't limit maximum size of share to backup
my $max_file_size;	# don't limit maximum file size to backup
my $min_free_space = 100000;	# leave 100Mb on backup destination
my $only;

my $help;

my $result = GetOptions(
        "ping!" => \$use_ping, "backupdest=s" => \$BACKUP_DEST,
	"verbose+" => \$verbose, "quiet+" => \$quiet,
	"email=s" => \$email,
	"max_share_size=i" => \$max_share_size,
	"max_file_size=i" => \$max_file_size,
	"min_free_space=i" => \$min_free_space,
	"help!" => \$help,
	"diff!" => \$diff,
	"only=s", => \$only,
);

if ($help) {
	pod2usage(-verbose => 2);
}

$verbose -= $quiet;

# diff mode settings
($verbose,$quiet) = (0,1) if ($diff);

my $mounts = shift @ARGV ||
	'mountscript';
#	die "usage: $0 mountscript";

my $basedir = $0;
$basedir =~ s,/?[^/]+$,,g;

# default subject for e-mail messages
my @subjects = ('Backup needs your attention!');
my $sub_nr = 0;
my $email_body;

my $home_dir=$ENV{'HOME'};
$home_dir = '/tmp' if (! -w $home_dir);

if ($email) {
	# It will use (and require) Tie::File only if --email=foo@bar.com
	# arguement is used!
	use Tie::File;
	tie @subjects, 'Tie::File', "$basedir/subjects.txt" || xlog("CONFIG","Can't find $basedir/subjects.txt... using default (only one)");
	chdir; # this will change directory to HOME
	if (open(my $sn, '<', "$home_dir/.psinib.subject")) {
		$sub_nr = <$sn>;
		chomp($sub_nr);
		close($sn);
	}
	$sub_nr++;
	# skip comments in subjects.txt
	while($subjects[$sub_nr] && $subjects[$sub_nr] =~ m/^#/) {
		$sub_nr++;
	}
	$sub_nr = 0 if (! $subjects[$sub_nr]);

	if (open(my $sn, '>', "$home_dir/.psinib.subject")) {
		print $sn "$sub_nr\n";
		close ($sn);
	} else {
		xlog("CONFIG","Can't open $home_dir/.psinib.subject -- I can't cycle subjects...");
	};
}

my @in_backup;	# shares which are backeduped this run

# init Net::Ping object
my $ping;
$ping = new Net::Ping->new("syn", 2) if ($use_ping);

# do syn ping to cifs port
sub host_up {
	my $ping = shift || return;
	my $host_ip = shift || xlog("host_up didn't get IP");
	my $timeout = shift;
	return 1 if (! $use_ping);

	# check various ports to see if host if up
	foreach my $port (qw(netbios-ns netbios-dgm netbios-ssn microsoft-ds)) {
		$ping->{port_num} = getservbyname($port, "tcp");
		$ping->ping($host_ip);
	}

	my $return = 0;

	while (my ($host,$rtt,$ip) = $ping->ack) {
		$return++ if ($ip eq $host_ip);
	}

	xlog("","HOST: $host_ip ACKed $return times") if ($return);

	return $return;
}

my $backup_ok = 0;

my $smb;
my %smb_atime;
my %smb_mtime;
my %file_md5;

open(my $m_fh, '<', $mounts) || die "can't open mountscript '$mounts': $!";
while(<$m_fh>) {
	chomp;
	next if !/^\s*smbmount\s/;
	my (undef,$share,undef,$opt) = split(/\s+/,$_,4);

	next if ($only && $share !~ m/\Q$only\E/);

	my ($user,$passwd,$workgroup,$ip);

	foreach (split(/,/,$opt)) {
		my ($n,$v) = split(/=/,$_,2);
		if ($n =~ m/username/i) {
			if ($v =~ m#^(.+)/(.+)%(.+)$#) {
				($user,$passwd,$workgroup) = ($1,$2,$3);
			} elsif ($v =~ m#^(.+)/(.+)$#) {
				($user,$workgroup) = ($1,$2);
			} elsif ($v =~ m#^(.+)%(.+)$#) {
				($user,$passwd) = ($1,$2);
			} else {
				$user = $v;
			}
		} elsif ($n =~ m#workgroup#i) {
			$workgroup = $v;
		} elsif ($n =~ m#ip#i) {
			$ip = $v;
		}
	}

	push @in_backup,$share;


	my ($host,$dir,$date_dir) = share2host_dir($share);
	my $bl = "$BACKUP_DEST/$host/$dir/latest";	# latest backup
	my $bc = "$BACKUP_DEST/$host/$dir/$date_dir";	# current one
	my $real_bl;
	if (-l $bl) {
		$real_bl=readlink($bl) || die "can't read link $bl: $!";
		$real_bl="$BACKUP_DEST/$host/$dir/$real_bl" if (substr($real_bl,0,1) ne "/");
		if (-l $bc && $real_bl eq $bc) {
			xlog($share,"allready backuped...");
			$backup_ok++;
			next;
		}

	}


	xlog($share,"working on $share...");

	# try to nmblookup IP
	$ip = get_ip($share) if (! $ip);

	if ($ip) {
		xlog($share,"IP is $ip");
		if (host_up($ping, $ip,$PING_TIMEOUT)) {
			if (snap_share($share,$user,$passwd,$workgroup)) {
				$backup_ok++;
			}
		}
	}
}
close($m_fh);

my $total = ($#in_backup + 1) || 0;
my $pcnt = "";
$pcnt = "(".int($backup_ok*100/$total)." %)" if ($total > 0);
xlog("","$backup_ok backups completed of total $total this time".$pcnt);

send_email();

1;

#-------------------------------------------------------------------------


# get IP number from share
sub get_ip {
	my $share = shift;

	my $host;
	if ($share =~ m#//([^/]+)/#) {
		$host = lc($1);
	} else {
		die "can't find host in share $share\n";
	}

	return $ip_cache{$host} if (defined($ip_cache{$host}));

	my $ip = `nmblookup $host`;
	if ($ip =~ m/(\d+\.\d+\.\d+\.\d+)\s$host/i) {
		$ip_cache{$host} = $1;
		return $1;
	}

	$ip = `host $host`;
	if ($ip =~ m/^$host.+\s+(\d+\.\d+\.\d+\.\d+)$/i) {
		$ip_cache{$host} = $1;
		return $1;
	}
}

# send e-mail with all messages
sub send_email {
	return if (! $email || $email eq "" || !$email_body);
	require Mail::Send;
	my $msg = new Mail::Send;
	$msg->to($email);
	$msg->subject($subjects[$sub_nr]);
	my $fn=$msg->open;
	print $fn $email_body;
	$fn->close;
}
	

# write entry to screen and log
sub xlog {
	my $share = shift;
	my $t = strftime $LOG_TIME_FMT, localtime;
	my $m = shift || '[no log entry]';
	my $l = shift;
	$l = 1 unless (defined $l);	# default verbosity is 1
	$verbose = 0 unless (defined $verbose);
	if ($verbose >= $l) {
		if (! $email) {
			print STDERR $m,"\n";
		# don't e-mail mesages with verbosity < 1
		} elsif ($l < 1) {
			$email_body .= $m."\n";
		}
	}
	print $log_fh "$t $share\t$m\n";
}


# split share name to host, dir and currnet date dir
sub share2host_dir {
	my $share = shift;
	my ($host,$dir);
	if ($share =~ m#//([^/]+)/(.+)$#) {
		($host,$dir) = ($1,$2);
		$dir =~ s/\W/_/g;
		$dir =~ s/^_+//;
		$dir =~ s/_+$//;
	} else {
		xlog($share,"Can't parse share $share into host and directory!",1);
		return;
	}
	return ($host,$dir,strftime $DIR_TIME_FMT, localtime);
}


# make a snapshot of a share
sub snap_share {

	my $share = shift;

	my %param = ( debug => 0 );

	$param{username} = shift || warn "can't find username for share $share";
	$param{password} = shift || warn "can't find passwod for share $share";
	$param{workgroup} = shift || warn "can't find workgroup for share $share";

	my ($host,$dir,$date_dir) = share2host_dir($share);

	# latest backup directory
	my $bl = "$BACKUP_DEST/$host/$dir/latest";
	# current backup directory
	my $bc = "$BACKUP_DEST/$host/$dir/$date_dir";

	my $real_bl;
	if (-l $bl) {
		$real_bl=readlink($bl) || die "can't read link $bl: $!";
		$real_bl="$BACKUP_DEST/$host/$dir/$real_bl" if (substr($real_bl,0,1) ne "/");
		if (! -e $real_bl) {
			xlog($share,"latest link $bl -> $real_bl not valid, removing it");
			unlink $bl || die "can't remove link $bl: $!";
			undef $real_bl;
		}
	}
	if (! $real_bl) {
		xlog($share,"no old backup, trying to find last backup");
		if (opendir(BL_DIR, "$BACKUP_DEST/$host/$dir")) {
			my @bl_dirs = sort grep { !/^\./ && -d "$BACKUP_DEST/$host/$dir/$_" } readdir(BL_DIR);
			closedir(BL_DIR);
			if ( $real_bl=pop @bl_dirs ) {
				xlog($share,"using $real_bl as latest...");
				$real_bl="$BACKUP_DEST/$host/$dir/$real_bl" if (substr($real_bl,0,1) ne "/");
				if ($real_bl eq $bc) {
					xlog($share,"latest from today (possible partial backup)");
					rename $real_bl,$real_bl.".partial" || warn "can't reaname partial backup: $!";
					$real_bl .= ".partial";
				}
			} else {
				xlog($share,"can't find last backup, assuming this is first one...\n");
			}
		} else {
			xlog($share,"this is first run...");
		}
	}

	if (-l $bc && $real_bl && $real_bl eq $bc) {
		xlog($share,"allready backuped...");
		return 1;
	}

	die "You should really create BACKUP_DEST [$BACKUP_DEST] by hand! " if (!-e $BACKUP_DEST);

	if (! -e "$BACKUP_DEST/$host") {
		mkdir "$BACKUP_DEST/$host" || die "can't make dir for host $host, $BACKUP_DEST/$host: $!";
		xlog($share,"created host directory $BACKUP_DEST/$host...");
	}

	if (! -e "$BACKUP_DEST/$host/$dir") {
		mkdir "$BACKUP_DEST/$host/$dir" || die "can't make dir for share $share, $BACKUP_DEST/$host/$dir $!";
		xlog($share,"created dir for this share $BACKUP_DEST/$host/$dir...");
	}

	mkdir $bc || die "can't make dir for current backup $bc: $!";

	my @dirs = ( "/" );
	my @smb_dirs = ( "/" );

	my $transfer = 0;	# bytes transfered over network

	# this will store all available files and sizes
	my @files;
	my %file_size;
	my %file_atime;
	my %file_mtime;
	%file_md5 = ();

	my @smb_files;
	my %smb_size;
	#my %smb_atime;
	#my %smb_mtime;

	sub norm_dir {
		my $dir = shift;
		my $prefix = shift;
		$dir =~ s#//+#/#g;
		$dir =~ s#/+$##g;
		$dir =~ s#^/+##g;
		$dir = $prefix.$dir if ($prefix);
		if ($dir =~ m!^smb://([^/]+)/!) {
			if (my $ip=get_ip($dir)) {
				$dir =~ s!^smb://$1/!smb://$ip/!;
			}
		}
		return $dir;
	}

	# read local filesystem
	my $di = 0;
	while ($di <= $#dirs && $real_bl) {
		my $d=$dirs[$di++];
		opendir(DIR,"$real_bl/$d") || warn "opendir($real_bl/$d): $!\n";

		# read .backupignore if exists
		if (-f "$real_bl/$d/.backupignore") {
			open(my $i,'<',"$real_bl/$d/.backupignore");
			while(<$i>) {
				chomp;
				push @ignore,norm_dir("$d/$_");
			}
			close($i);
#print STDERR "ignore: ",join("|",@ignore),"\n";
			link "$real_bl/$d/.backupignore","$bc/$d/.backupignore" ||
				warn "can't copy $real_bl/$d/.backupignore to current backup dir: $!\n";
		}

		# read .md5sum if exists
		if (-f "$real_bl/$d/.md5sum") {
			open(my $m,'<',"$real_bl/$d/.md5sum");
			while(<$m>) {
				chomp;
				my ($md5,$f) = split(/\s+/,$_,2);
				$file_md5{$f}=$md5;
			}
			close($m);
		}

		my @clutter = readdir(DIR);
		foreach my $f (@clutter) {
			next if ($f eq '.');
			next if ($f eq '..');
			my $pr = norm_dir("$d/$f");	# path relative
			my $pf = norm_dir("$d/$f","$real_bl/");	# path full
			if (grep(/^\Q$pr\E$/,@ignore) == 0) {
				if (-f $pf) {
					my $size = (stat($pf))[7];
					if ($max_file_size && ($size/1024) > $max_file_size) {
						xlog($share,"local file '$pf' (".int($size/1024)." Kb) larger than $max_file_size Kb, skipping",0);
						xlog("INFO","strictly speaking, this shouldn't happend and it will trigger backup of remote file if it's smaller than $max_file_size Kb, but if you changed --max-file-size option it's expected.",1); 
					} else {
						push @files,$pr;
						$file_size{$pr}= $size;
						$file_atime{$pr}=(stat($pf))[8];
						$file_mtime{$pr}=(stat($pf))[9];
					}
				} elsif (-d $pf) {
					push @dirs,$pr;
				} else {
					xlog($share,"not file or directory: $pf",0);
					print "? $share $pf\n" if ($diff);
				}
			} else {
				xlog($share,"ignored: $pr");
				print "I $share $pf\n" if ($diff && $verbose);
			}
		}
	}

	# local dir always include /
	xlog($share,($#files+1)." files and ".($#dirs)." dirs on local disk before backup");

	# read smb filesystem

	xlog($share,"smb to $share as $param{username}/$param{workgroup}");

	# FIX: how to aviod creation of ~/.smb/smb.conf ?
	$smb = new Filesys::SmbClient(%param) || die "SmbClient :$!\n";

	my $share_size = 0;

	$di = 0;
	while ($di <= $#smb_dirs) {
		my $d=$smb_dirs[$di];
		my $pf = norm_dir($d,"smb:$share/");	# path full
		my $D = $smb->opendir($pf);
		if (! $D) {
			xlog($share,"FATAL: $share [$pf] as $param{username}/$param{workgroup}: $!",0);
			# remove failing dir
			delete $smb_dirs[$di];
			return 0;			# failed
		}
		$di++;

		my @clutter = $smb->readdir_struct($D);
		foreach my $item (@clutter) {
			my $f = $item->[1];
			next if ($f eq '.');
			next if ($f eq '..');
			my $pr = norm_dir("$d/$f");	# path relative
			my $pf = norm_dir("$d/$f","smb:$share/"); # path full
			if (grep(/^\Q$pr\E$/,@ignore) == 0) {
				if ($item->[0] == main::SMBC_FILE) {
					my $size = ($smb->stat($pf))[7];
					if ($max_file_size && ($size/1024) > $max_file_size) {
						xlog($share,"file '$pf' (".int($size/1024)." Kb) larger than $max_file_size Kb, skipping",0);
					} else {
						push @smb_files,$pr;
						$smb_size{$pr}= $size;
						$smb_atime{$pr}=($smb->stat($pf))[10];
						$smb_mtime{$pr}=($smb->stat($pf))[11];
						$share_size += $size;
					}
				} elsif ($item->[0] == main::SMBC_DIR) {
					push @smb_dirs,$pr;
				} else {
					xlog($share,"not file or directory [".$item->[0]."]: $pf",0);
				}
			} else {
				xlog($share,"smb ignored: $pr");
			}
		}
	}

	xlog($share,($#smb_files+1)." files and ".($#smb_dirs)." dirs on remote share");

	# sync dirs
	my $lc = List::Compare->new(\@dirs, \@smb_dirs);

	my @dirs2erase = $lc->get_Lonly;
	my @dirs2create = $lc->get_Ronly;
	xlog($share,($#dirs2erase+1)." dirs to erase and ".($#dirs2create+1)." dirs to create");

	# check if share is too big to backup
	if ($max_share_size && ($share_size/1024) > $max_share_size) {
		xlog($share,"remote share is larger than $max_share_size Kb, backup skipped",0);
		return 0;
	}

	# check if local disk has enough free space for backup
	if ($min_free_space) {
		my $df_to = df($BACKUP_DEST, 1024)->{'bavail'} || die "can't get free space for '$BACKUP_DEST': $!";
		xlog($share,"free space on '$BACKUP_DEST' is $df_to Kb",2);
		if ($df_to < $min_free_space) {
			xlog($share,"local filesystem '$BACKUP_DEST' has only $df_to Kb available (minimum is $min_free_space), backup aborted",0);
			return 0;
		}
	}

	# create new dirs
	foreach (sort @smb_dirs) {
		next if -e "$bc/$_";
		mkdir "$bc/$_" || warn "mkdir $_: $!\n";
		print "+ $bc/$_\n" if ($diff);
	}

	# sync files
	$lc = List::Compare->new(\@files, \@smb_files);

	my @files2erase = $lc->get_Lonly;
	my @files2create = $lc->get_Ronly;
	xlog($share,($#files2erase+1)." files to erase and ".($#files2create+1)." files to create");

	sub smb_copy {
		my $smb = shift;

		my $from = shift;
		my $to = shift;


		my $l = 0;
		
		foreach my $f (@_) {
			next if $f =~ m/\.md5sum$/;

			print "C $from/$f $to/$f\n" if ($diff);
			print "smb_copy $from/$f -> $to/$f\n" if ($verbose);

			my $md5 = Digest::MD5->new;

			my $fd = $smb->open("$from/$f");
			if (! $fd) {
				xlog("WARNING","can't open smb file $from/$f: $!");
				next;
			}

			my $local_fh;
			my $local_path = "$to/$f";
			$local_path =~ s!^(.+/)([^/]+)$!$1.$2!;		# add dot before filename

			if (! open($local_fh, '>', $local_path)) {
				xlog("WARNING","can't open new file $local_path: $!");
				next;
			}

			while (my $b=$smb->read($fd,4096)) {
				print $local_fh $b;
				$l += length($b);
				$md5->add($b);
			}

			$smb->close($fd);
			close($local_fh) || warn "can't close $from/$f: $!";

			rename $local_path, "$to/$f" || xlog("WARNING", "SKIPPING can't rename $local_path -> $to/$f: $!");

			$file_md5{$f} = $md5->hexdigest;

			# FIX: this fails with -T
			my ($a,$m) = ($smb->stat("$from/$f"))[10,11];
			utime $a, $m, "$to/$f" ||
				warn "can't update utime on $to/$f: $!\n";

		}
		return $l;
	}

	# copy new files
	foreach (@files2create) {
		$transfer += smb_copy($smb,"smb:$share",$bc,$_);
	}

	my $size_sync = 0;
	my $atime_sync = 0;
	my $mtime_sync = 0;
	my @sync_files;
	my @ln_files;

	foreach ($lc->get_intersection) {

		my $f;

		if ($file_size{$_} != $smb_size{$_}) {
			$f=$_;
			$size_sync++;
		}
		if ($file_atime{$_} != $smb_atime{$_}) {
			$f=$_;
			$atime_sync++;
		}
		if ($file_mtime{$_} != $smb_mtime{$_}) {
			$f=$_;
			$mtime_sync++;
		}

		if ($f) {
			push @sync_files, $f;
		} else {
			push @ln_files, $_;
		}
	}

	xlog($share,($#sync_files+1)." files will be updated (diff: $size_sync size, $atime_sync atime, $mtime_sync mtime), ".($#ln_files+1)." will be linked.");

	foreach (@sync_files) {
		$transfer += smb_copy($smb,"smb:$share",$bc,$_);
	}

	xlog($share,sprintf("%1.2f Kb transfered...",$transfer/1024));

	foreach (@ln_files) {
		link "$real_bl/$_","$bc/$_" || warn "link $real_bl/$_ -> $bc/$_: $!\n";
	}

	# remove files
	foreach (sort @files2erase) {
		unlink "$bc/$_" || warn "unlink $_: $!\n";
		delete $file_md5{$_};
		print "-f $file_md5{$_}\n" if ($diff);
	}

	# remove not needed dirs (after files)
	foreach (sort @dirs2erase) {
		if (-d $_) {
			rmtree("$bc/$_",1,1) || warn "rmtree $bc/$_: $!\n";
			print "-d $file_md5{$_}\n" if ($diff);
		} else {
			xlog("NOTICE","can't rmtree '$bc/$_' which doesn't exist!");
		}
	}

	# remove old .md5sum
	foreach (sort @dirs) {
		unlink "$bc/$_/.md5sum" if (-e "$bc/$_/.md5sum");
	}

	# erase stale entries in .md5sum
	my @md5_files = keys %file_md5;
	$lc = List::Compare->new(\@md5_files, \@smb_files);
	foreach my $file ($lc->get_Lonly) {
		xlog("NOTICE","removing stale '$file' from .md5sum");
		delete $file_md5{$file};
	}

	# create .md5sum
	my $last_dir = '';
	my $md5;
	foreach my $f (sort { $file_md5{$a} cmp $file_md5{$b} } keys %file_md5) {
		my $dir = dirname($f);
		my $file = basename($f);
#print "$f -- $dir / $file<--\n";
		if ($dir ne $last_dir) {
			close($md5) if ($md5);
			open($md5, '>>', "$bc/$dir/.md5sum") || warn "can't create $bc/$dir/.md5sum: $!";
			$last_dir = $dir;
#print STDERR "writing $last_dir/.md5sum\n";
		}
		print $md5 $file_md5{$f},"  $file\n";
	}
	close($md5) if ($md5);

	# create leatest link
#print "ln -s $bc $real_bl\n";
	if (-l $bl) {
		unlink $bl || warn "can't remove old latest symlink $bl: $!\n";
	}
	symlink $bc,$bl || warn "can't create latest symlink $bl -> $bc: $!\n";

	# FIX: sanity check -- remove for speedup
	xlog($share,"failed to create latest symlink $bl -> $bc...") if (readlink($bl) ne $bc || ! -l $bl);

	xlog($share,"backup completed...");

	return 1;
}
__END__
#-------------------------------------------------------------------------

=head1 NAME

psinib - Perl Snapshot Is Not Incremental Backup

=head1 SYNOPSIS

  ./psinib.pl [OPTION]... [mount script]

=head1 DESCRIPTION

Backup samba or Windows shares using LinNeighborhood mount script as
configuration

Option can be one of more of following:

=over 8

=item C<--backupdest=dir>

Specify backup destination directory (default is /backup/)

=item C<--noping>

Don't use ping to check if host is up (default is to use TCP SYN to CIFS
port)

=item C<--verbose -v>

Increase verbosity level. Default is 1 which prints moderate amount of data
on STDOUT and STDERR.

=item C<--quiet -q>

Decrease verbosity level

=item C<--email=email@domain>

Send e-mails instead of dumping errors to STDERR. Useful for cron jobs.

=item C<--max_share_size=50000>

Specify maximum size of share to backup (in Kb).
This example will skip backuping shares which have more than 50Mb.
By default this option is disabled.

=item C<--max_file_size=5000>

Specify maximum size of file to backup (in Kb).
This example will limit file size to 5Mb.
By default this option is disabled.

=item C<--min_free_space=0>

Specify minumum free space on destination file system to start backup. If
space is not availabe, backup will abort.
By default, 100Mb of free space is required on destination. You can set
this value to 0 to disable this check.

=item C<--diff>

Show compact diff-like output for changed files

=item C<--only=part_of_share_name>

Backup just shares which match C<part_of_share_name> in this run.

=item C<--help -h>

Display usage page.

=back

Idea of this script is to produce tool which will be easy enough to use
for any Linux administrator which has a task to backup Windows workstations
or samba shares. Backup is done to central disk space on Linux server
running B<psinib>. It's organized in multiple directories named after:

=over 4

=item *
server which is sharing files to be backed up

=item *
name of share on server

=item *
dated directory named like standard ISO date format (YYYYMMDD).

=back

In each dated directory you will find I<snapshot> of all files on
exported share on that particular date.

You can also use symlink I<latest> which will lead you to
last completed backup. After that you can use some other backup
software to transfer I<snapshot> to tape, CD-ROM or some other media, or
just re-export it via samba so that users can do restore of files on
specific date themselves.

=head2 Design considerations

Since taking of share snapshot every day requires a lot of disk space and
network bandwidth, B<psinib> uses several techniques to keep disk usage and
network traffic at acceptable level:

=over 3

=item - usage of hard-links to provide same files in each snapshot (as opposed
to have multiple copies of same file)

=item - usage of file size, atime and mtime to find changes of files without
transferring whole file over network (just share browsing is transfered
over network)

=item - usage of C<.md5sum> files (compatible with command-line utility
C<md5sum>) to keep file between snapshots hard-linked

=back

=head1 CONFIGURATION

Configuration file format is simple:

 smbmount //WIN_BOX/data /home/dpavlin/mnt/WIN_BOX/data/ -o username=dpavlin%password,fmask=644,dmask=755,uid=1000,gid=1000,debug=0,workgroup=HOME

This file is produced by LinNeighborhood L<http://www.bnro.de/~schmidjo/>
utility. So to make backup of your Windows share, it's enough just to mount
it using LinNeighborhood, and use C<Export Mountscript> to produce
mountscript which is at the same time configuration file for B<psinib>.

=head1 HACKS, TRICKS, BUGS and LIMITATIONS

This chapter will have all content that doesn't fit anywhere else.

=head2 Can snapshots be more frequent than daily?

There is not real reason why you can't take snapshot more often than
once a day. Actually, if you are using B<psinib> to backup Windows
workstations you already know that they tend to come-and-go during the day
(reboots probably ;-), so running B<psinib> several times a day increases
your chance of having up-to-date backup (B<psinib> will not make multiple
snapshots for same day, nor will it update snapshot for current day if
it already exists).

However, changing B<psinib> to produce snapshots which are, for example, hourly
is a simple change of C<$DIR_TIME_FMT> which is currently set to
C<'%Y%m%d'> (see I<strftime> documentation for explanation of that 
format). If you change that to C<'%Y%m%d-%H> you can have hourly snapshots
(if your network is fast enough, that is...). Also, some of messages in
program will sound strange, but other than that it should work.
I<You have been warned>.

=head2 Do I really need to share every directory which I want to snapshot?

Actually, no. Due to usage of C<Filesys::SmbClient> module, you can also
specify sub-directory inside your share that you want to backup. This feature
is most useful if you want to use administrative shares (but, have in mind
that you have to enter your Win administrator password in unencrypted file on
disk to do that) like this:

	smbmount //server/c$/WinNT/fonts  /mnt  -o username=administrator%win  

After that you will get directories with snapshots like:

	server/c_WinNT_fonts/yyyymmdd/....

=head2 Won't I run out of disk space?

Of course you will... Snapshots and logfiles will eventually fill-up your disk.
However, you can do two things to stop that:

=head3 Clean snapshot older than x days

You can add following command to your C<root> crontab:

	find /backup/isis_backup -type d -mindepth 3 -maxdepth 3 -mtime +11 -exec rm -Rf {} \;

I assume that C</backup/isis_backup> is directory in which are your snapshots
and that you don't want to keep snapshots older than 11 days (that's
C<-mtime +11> part of command).

=head3 Rotate your logs

I will leave that to you. I relay on GNU/Debian's C<logrotate> to do it for me.

=head2 What are I<YYYYMMDD.partial> directories?

If there isn't I<latest> symlink in snapshot directory, it's pretty safe to
assume that previous backup from that day failed. So, that directory will
be renamed to I<YYYYMMDD.partial> and snapshot will be performed again,
linking same files (other alternative would be to erase that dir and find
second-oldest directory, but this seemed like more correct approach).

=head2 I can't connect to any share

Please verify that nmblookup (which is part of samba package) is in /bin or
/usr/bin. Also verify that nmblookup returns IP address for your server
using:

   $ nmblookup tvhouse
   querying tvhouse on 192.168.34.255
   192.168.34.30 tvhouse<00>

If you don't get any output, your samba might not listen to correct interface
(see interfaces in smb.conf).

=head2 Aren't backups boring?

No! If you have subjects.txt in same directory as C<psinib.pl> you can get
various funny subjects in your mail. They change over time as long as you
ignore your backup.

=head2 Why all those limit on backup share and files?

Well, if share is too big or disk space on destionation directory is too
small, this might create denial of service attack on your server.
By default, only free space on destination directory is checked.

=head1 AUTHOR

Dobrica Pavlinusic <dpavlin@rot13.org>

L<http:E<sol>E<sol>www.rot13.orgE<sol>~dpavlinE<sol>>

=head1 LICENSE

This product is licensed under GNU Public License (GPL) v2 or later.

=cut
