#!/usr/bin/perl -w

use strict;
use DBI;
use Quota;
use IO::Handle;
use IO::Select;
use POSIX qw(strftime);

use vars qw(
	$acctserver $user $pass $temptable
	$home $mode $quotadev $make_changes
);

$pass = "";
$make_changes = 1;

die "Usage: $0 configfile" unless $#ARGV == 0;
require $ARGV[0];

sub action_remove_user($) {
	my ($user) = @_;
	printf "moving old home of %8s to OLD/%s-%s\n", $user, $user, strftime('%F-%T', localtime);
	if ($make_changes) {
		if (! -e 'OLD') {
			mkdir 'OLD';
		} elsif (! -d 'OLD') {
			print "error: OLD exists, but is not a directory!\n";
			return 0;
		}
		return rename($user, "OLD/".$user."-".strftime('%F-%T', localtime));
	}
	return 1;
}

sub action_change_username($$) {
	my ($old, $new) = @_;
	if ($make_changes && -e $new) {
		print "homedir $new already exits";
		return 0;
	}
	printf "renaming \"%8s\" to \"%8s\" (username changed)\n", $old, $new;
	if ($make_changes) {
		return rename($old, $new);
	}
	return 1;
}

sub action_create_user($$$) {
	my ($uid, $gid, $user) = @_;
	if ($make_changes && -e $user) {
		print "homedir $user already exits";
		return 0
	}
	printf "creating home for new user %8s(%d.%d)\n", $user, $uid, $gid;
	if ($make_changes) {
		return 0 unless (mkdir($user, $mode));
		chown $uid, $gid, $user;
	}
	return 1;
}

sub action_update_quota($$$$$$) {
	my ($user, $uid, $b_soft, $b_hard, $i_soft, $i_hard) = @_;
	return unless defined $quotadev;
	printf "setting quota for %8s(%d): %s %s %s %s %s\n", $user, $uid, $b_soft, $b_hard, $i_soft, $i_hard, $quotadev;
	if ($make_changes) {
		Quota::setqlim($quotadev, $uid, $b_soft, $b_hard, $i_soft, $i_hard, 1, 0);
	}
	return 1
}
#
###################
#
chdir($home) || die "can't chdir to $home: $!";

sub doconn() {
	my $dbh = DBI->connect($acctserver, $user, $pass, { RaiseError => 1, AutoCommit => 1, PrintError => 1 });
	unless (defined($dbh)) {
		printf STDERR "can't connect to database: %s\n", $DBI::errstr;
		exit 1;
	}
	
	$dbh->do("LISTEN syncusers");
	
	return $dbh;
}

sub dir2data($) {
	my ($dir) = @_;
	my ($uid, $gid) = (stat($dir))[4,5];
	my (undef, $b_soft, $b_hard, undef, undef, $i_soft, $i_hard, undef) = defined($quotadev) ? Quota::query($quotadev, $uid) : ();
	$b_soft = 0 unless (defined $b_soft);
	$b_hard = 0 unless (defined $b_hard);
	$i_soft = 0 unless (defined $i_soft);
	$i_hard = 0 unless (defined $i_hard);
	return ($uid, $gid, $dir, $b_soft, $b_hard, $i_soft, $i_hard);
}

sub doinit($) {
	my ($dbh) = @_;
	my ($dirent, @data);
	
	$dbh->do("CREATE TEMP TABLE $temptable ".q<(
		uid int PRIMARY KEY,
		gid int NOT NULL,
		username text UNIQUE NOT NULL CHECK (CASE
			WHEN (username LIKE 'NEW-%') THEN (username ~ '^NEW-[a-z]{1,8}$')
			ELSE (username ~ '^[a-z]{1,8}$') END),
		b_soft int NOT NULL DEFAULT 0,
		b_hard int NOT NULL DEFAULT 0,
		i_soft int NOT NULL DEFAULT 0,
		i_hard int NOT NULL DEFAULT 0
	);>);
	
	my $ins = $dbh->prepare("INSERT INTO $temptable
		(uid, gid, username, b_soft, b_hard, i_soft, i_hard)
		VALUES (?, ?, ?, ?, ?, ?, ?)");
	
	opendir(DIR, ".") || die "can't opendir $home: $!";
	while ($dirent = readdir(DIR)) {
		next unless ($dirent =~ /^[a-z]{1,8}$/);
		next unless (-d $dirent);
		@data = dir2data($dirent);
		next if ($data[0] == 0);
		$ins->execute(@data); # breaks on duplicated uids
	}
	closedir(DIR);
	$ins->finish; undef $ins;
}

sub remove_user {
	my ($dbh, $user) = @_;
	return unless action_remove_user($user);
	$dbh->do("DELETE FROM $temptable WHERE username = ?", {}, $user);
}

sub change_username {
	my ($dbh, $old, $new) = @_;
	return unless action_change_username($old, $new);
	$dbh->do("UPDATE $temptable SET username = ? WHERE username = ?", {}, $new, $old);
}

sub create_user {
	my ($dbh, $uid, $gid, $user) = @_;
	return unless action_create_user($uid, $gid, $user);
	$dbh->do("INSERT INTO $temptable (uid, gid, username) VALUES (?, ?, ?)", {}, $uid, $gid, $user);
}

sub update_quota {
	my ($dbh, $user, $uid, $b_soft, $b_hard, $i_soft, $i_hard) = @_;
	return unless action_update_quota($user, $uid, $b_soft, $b_hard, $i_soft, $i_hard);
	$dbh->do("UPDATE $temptable SET b_soft = ?, b_hard = ?, i_soft = ?, i_hard = ? WHERE uid = ?", {},
		$b_soft, $b_hard, $i_soft, $i_hard, $uid);
}

sub dosync_remove_user($) {
	my ($dbh) = @_;
	my $arr = $dbh->selectall_arrayref("SELECT h.username FROM $temptable h LEFT JOIN synchome_pg p USING (uid) WHERE p.uid IS NULL");
	foreach my $row (@{$arr}) {
		remove_user($dbh, @{$row});
	}
}

sub dosync_change_username($) {
	my ($dbh) = @_;
	my $arr = $dbh->selectall_arrayref("SELECT h.username, p.username FROM $temptable h JOIN synchome_pg p USING (uid) WHERE h.username != p.username");
	foreach my $row (@{$arr}) {
		change_username($dbh, $row->[0], "NEW-".$row->[1]);
	}
	foreach my $row (@{$arr}) {
		change_username($dbh, "NEW-".$row->[1], $row->[1]);
	}
}

sub dosync_create_user($) {
	my ($dbh) = @_;
	my $arr = $dbh->selectall_arrayref("SELECT p.uid, p.gid, p.username FROM $temptable h RIGHT JOIN synchome_pg p USING (uid) WHERE h.uid IS NULL");
	foreach my $row (@{$arr}) {
		create_user($dbh, @{$row});
	}
}

sub dosync_update_quota($) {
	my ($dbh) = @_;
	my $arr = $dbh->selectall_arrayref("SELECT p.username, p.uid, p.b_soft, p.b_hard, p.i_soft, p.i_hard FROM $temptable h JOIN synchome_pg p USING (uid) WHERE h.b_soft != p.b_soft OR h.b_hard != p.b_hard OR h.i_soft != p.i_soft OR h.i_hard != p.i_hard");
	foreach my $row (@{$arr}) {
		update_quota($dbh, @{$row});
	}
}

sub dosync($) {
	my ($dbh) = @_;
	dosync_remove_user($dbh);
	dosync_change_username($dbh);
	dosync_create_user($dbh);
	dosync_update_quota($dbh);
}

my ($sel, $fd, $dbh);
for (; 1; sleep 10) {
	eval {
		print("connecting...\n");
		$dbh = doconn();
		print("doing init...\n");
		doinit($dbh);
		print "doing initial sync...\n";
		dosync($dbh);
		$fd = IO::Handle->new_from_fd($dbh->func('getfd'), "r+");
		$sel = IO::Select->new($fd);
		while (1) {
			print "waiting for changes...\n";
			$sel->can_read();
			print "got something...\n";
			if (defined($dbh->func('pg_notifies'))) {
				print "doing sync...\n";
				dosync($dbh);
			}
			die "connection lost" unless ($dbh->ping);
		}
	};
	if ($@) {
		print $@;
		print "=> reseting!\n";
	}
	eval {
		$dbh->disconnect if defined($dbh);
	};
	print $@ if ($@);
	undef $dbh;
	print "connection lost. sleeping befor reconnect\n";
}

#### 

exit 0;
1;
