# -*- perl -*-
#
# $Id: QsyncConfig.pm,v 1.2 1999/02/12 19:56:31 ejb Exp $
# $Source: /home/ejb/source/qsync/util/qsutil_modules/RCS/QsyncConfig.pm,v $
# $Author: ejb $
#

require 5.002;
use strict;

package QsyncConfig;
my $package = "QsyncConfig";

require SambaSpec;

# Field names
my $f_whoami = "whoami";
my $f_data_dir = "data_dir";
my $f_sync_dir = "sync_dir";
my $f_conf_file = "conf_file";
my $f_actions = "actions";
my $f_tarlist = "tarlist";
my $f_olddata = "olddata";
my $f_diff = "diff";
my $f_config = "config";
my $f_other_umask = "other_umask";
my $f_live = "live";
my $f_samba = "samba";
my $f_samba_config = "samba_config";

# Static Variables
my $conf_name = "qsync.conf";

# Samba configuration fields
my $smb_user = "user";
my $smb_pass = "pass";
my $smb_host = "host";
my $smb_service = "service";
my $smb_dir = "dir";

# Routines

sub new
{
    my $class = shift;
    my $rep = +{$package => {} };

    my $whoami = shift;
    my $sync_dir = shift;
    my $data_dir = (shift || $sync_dir);

    if ($sync_dir =~ m,^/,)
    {
	die "$whoami: sync_dir must be a relative path\n";
    }
    if (! -d $data_dir)
    {
	die "$whoami: data_dir ($data_dir) does not exist or is not a " .
	    "directory.\n";
    }

    # Configuration options from config file
    my $config =
	+{"filter" => "",
	  "last_sync" => "",
	  "new_sync" => "",
	  "peer" => "",
	  "rename_to" => "",
	  "live" => "",
	  "umask" => "",
	  "prune" => "",
	  "extra_qsfiles_flags" => "",
	  "backup" => "",
	  "exclude_cores" => "",
	  "junkfiles" => "",
	  "nonfile_mtimes" => "",
	  "ownerships" => "",
	  "one_filesys" => "",
	  "tmpdir" => "",
      };
    
    my $file = "$data_dir/$conf_name";

    $rep->{$package}{$f_whoami} = $whoami;
    $rep->{$package}{$f_sync_dir} = $sync_dir;
    $rep->{$package}{$f_data_dir} = $data_dir;
    $rep->{$package}{$f_conf_file} = $file;
    $rep->{$package}{$f_actions} = "$data_dir/actions";
    $rep->{$package}{$f_tarlist} = "$data_dir/tarlist";
    $rep->{$package}{$f_olddata} = "$data_dir/olddata";
    $rep->{$package}{$f_diff} = "$data_dir/diff";
    $rep->{$package}{$f_config} = $config;
    $rep->{$package}{$f_other_umask} = "";
    $rep->{$package}{$f_live} = 0;
    $rep->{$package}{$f_samba} = 0;
    $rep->{$package}{$f_samba_config} = undef;

    bless $rep, $class;
    $rep->p_read();

    if ($rep->get_config("live") ne "")
    {
	$rep->{$package}{$f_live} = 1;
    }

    $rep;
}

# Member accessors

sub p_whoami
{
    $_[0]->{$package}{$f_whoami};
}

sub sync_dir
{
    $_[0]->{$package}{$f_sync_dir};
}

sub data_dir
{
    $_[0]->{$package}{$f_data_dir};
}

sub p_config
{
    $_[0]->{$package}{$f_config};
}

sub samba_config
{
    my $rep = shift;
    $rep->{$package}{$f_samba_config};
}

sub samba
{
    $_[0]->{$package}{$f_samba};
}

sub live
{
    $_[0]->{$package}{$f_live};
}

# Routines

sub p_valid_config_entry
{
    my $rep = shift;
    my ($key, $val) = @_;

    my $whoami = $rep->p_whoami();
    my $config = $rep->p_config();

    my $result = 1;
    if (! exists($config->{$key}))
    {
	warn "$whoami: unrecognized configuration key $key\n";
	$result = 0;
    }
    else
    {
	if ($key eq "umask")
	{
	    if ($val !~ m/^[0-7]{1,3}$/)
	    {
		warn "$whoami: umask must be an octal number from 0 to 777\n";
		$result = 0;
	    }
	}
	elsif (($key eq "backup") ||
	       ($key eq "exclude_cores") ||
	       ($key eq "nonfile_mtimes") ||
	       ($key eq "ownerships") ||
	       ($key eq "one_filesys"))
	{
	    if (($val ne "true") && ($val ne "false"))
	    {
		warn "$whoami: $key must be true or false\n";
		$result = 0;
	    }
	}
	elsif ($key eq "junkfiles")
	{
	    if (($val ne "cleanup") &&
		($val ne "ignore") &&
		($val ne "preserve"))
	    {
		warn "$whoami: junkfiles must be cleanup, ignore, or " .
		    "preserve\n";
		$result = 0;
	    }
	}
	elsif ($key eq "tmpdir")
	{
	    if ($val !~ m,^/,)
	    {
		warn "$whoami: tmpdir must be an absolute path\n";
		$result = 0;
	    }
	}
	# else don't care
    }

    $result;
}

sub p_compute_dbnames
{
    my $rep = shift;

    my $data_dir = $rep->data_dir();

    # Must check rename_to before filling in defaults for last_sync and
    # new_sync
    my $last_sync = $rep->get_config("last_sync");
    my $new_sync = $rep->get_config("new_sync");
    my $peer = $rep->get_config("peer");
    my $rename_to = $rep->get_config("rename_to");

    if ($new_sync eq "")
    {
	$new_sync = "$data_dir/new_sync";
	if (($last_sync eq "") && ($rename_to eq ""))
	{
	    $rename_to = "$data_dir/last_sync";
	}
    }
    if ($last_sync eq "")
    {
	if ($peer eq "")
	{
	    $last_sync = "$data_dir/last_sync";
	}
	else
	{
	    $last_sync = "$peer/last_sync";
	}
    }

    $rep->set_config("last_sync", $last_sync);
    $rep->set_config("new_sync", $new_sync);
    $rep->set_config("rename_to", $rename_to);
}

sub p_validate_databases
{
    my $rep = shift;
    my $non_interactive = shift;

    my $whoami = $rep->p_whoami();

    my $last_sync = $rep->get_config("last_sync");
    my $new_sync = $rep->get_config("new_sync");
    my $rename_to = $rep->get_config("rename_to");

    if (! -f $last_sync)
    {
	die "$whoami: files database $last_sync does not exist\n";
    }

    if (-f $new_sync)
    {
	if (($rename_to eq "") || ($non_interactive))
	{
	    die "$whoami: $new_sync exists; exiting.\n";
	}
	else
	{
	    my $answer;
	    print "$whoami: $new_sync exists.\n";
	    print "   [1] Rename to $rename_to\n";
	    print "   [2] Remove\n";
	    print "==> ";
	    do {
		chop($answer = <STDIN>);
		if ($answer eq "1")
		{
		    print "$whoami: renaming to $rename_to\n";
		    unlink $rename_to;
		    rename($new_sync, $rename_to);
		}
		elsif ($answer eq "2")
		{
		    print "$whoami: removing $new_sync\n";
		    unlink $new_sync;
		}
		else
		{
		    print "Please enter either 1 or 2: ";
		}
	    } while (! (($answer eq "1") || ($answer eq "2")));
	}
    }
}

sub p_validate_live
{
    my $rep = shift;

    my $whoami = $rep->p_whoami();
    my $sync_dir = $rep->sync_dir();

    my $live_path = $rep->get_config("live");
    if ($live_path !~ m,^/,)
    {
	die "$whoami: live path must be absolute\n";
    }
    my $other_sync_dir = $live_path;
    $other_sync_dir =~ s,/?$,/$sync_dir,;
    if (! -d $other_sync_dir)
    {
	die "$whoami: $other_sync_dir does not exist or is not a directory.\n";
    }
}

sub p_validate_umask
{
    my $rep = shift;

    my $whoami = $rep->p_whoami();
    my $conf_file = $rep->conf_file();

    my $umask = $rep->get_config("umask");
    return if $umask eq "";
    my $peer = $rep->get_config("peer");
    return if $peer eq "";

    # Get umask from peer's configuration file.
    my $peer_config = "$peer/$conf_name";
    open(P, "<$peer_config") || die "$whoami: umask given but can't " .
	"open $peer_config: $!\n";
    my $peer_umask = "";
    while (<P>)
    {
	chop;
	if (m/^\s*umask\s*:\s*(\S+)\s*$/)
	{
	    $peer_umask = $1;
	    # don't stop -- later values override earlier values
	}
    }
    close(P);
    if ($peer_umask eq "")
    {
	die "$whoami: umask specified in $conf_file but not in $peer_config\n";
    }
    elsif ($peer_umask !~ m/^[0-7]{1,3}$/)
    {
	die "$whoami: peer umask must be an octal number from 0 to 777\n";
    }
    else
    {
	$rep->other_umask($peer_umask);
    }
}

sub p_set_config_if_empty
{
    my $rep = shift;
    my ($key, $val) = @_;

    if ($rep->get_config($key) eq "")
    {
	$rep->set_config($key, $val);
    }
}

sub p_compute_booleans
{
    my $rep = shift;

    $rep->p_set_config_if_empty("backup", "false");
    my $backup_mode = $rep->bool_config("backup");
    $rep->p_set_config_if_empty("exclude_cores",
				($backup_mode ? "false" : "true"));
    $rep->p_set_config_if_empty("junkfiles",
				($backup_mode ? "preserve" : "cleanup"));
    $rep->p_set_config_if_empty("nonfile_mtimes",
				($backup_mode ? "true" : "false"));
    $rep->p_set_config_if_empty("ownerships",
				($backup_mode ? "true" : "false"));
    $rep->p_set_config_if_empty("one_filesys",
				($backup_mode ? "true" : "false"));
}

sub p_compute_defaults
{
    my $rep = shift;

    $rep->p_compute_booleans();

    $rep->p_set_config_if_empty("tmpdir", "/tmp");
}

sub p_validate_filter
{
    my $rep = shift;

    my $whoami = $rep->p_whoami();
    my $data_dir = $rep->data_dir();

    my $filter = $rep->get_config("filter");
    if ($filter eq "")
    {
	my $t_filter = "$data_dir/filter";
	if (-f $t_filter)
	{
	    $filter = $t_filter;
	}
	else
	{
	    my $o_t_filter = $t_filter;
	    $t_filter = $data_dir;
	    if ($t_filter =~ s,/[^/]+$,,)
	    {
		$t_filter .= "/filter";
		if (-f $t_filter)
		{
		    $filter = $t_filter;
		}
		else
		{
		    die "$whoami: neither $o_t_filter nor $t_filter " .
			"exist or are files.\n";
		}
	    }
	    else
	    {
		die "$whoami: $o_t_filter does not exist or is not a file.\n";
	    }
	}
    }
    else
    {
	if (! -f $filter)
	{
	    die "$whoami: $filter does not exist or is not a file.\n";
	}
    }
    $rep->set_config("filter", $filter);
}

sub p_read
{
    my $rep = shift;

    my $whoami = $rep->p_whoami();
    my $file = $rep->conf_file();
    my $data_dir = $rep->data_dir();

    local(*F);
    my $error = 0;
    if (open(F, "<$file"))
    {
	binmode F;
	while (<F>)
	{
	    chop;
	    s/\#.*//;
	    s/^\s+//;
	    s/\s+$//;
	    next if m/^$/;
	    
	    if (m/^([^:]+)\s*:\s*(\S.*)$/)
	    {
		# set_config calls valid_config_entry but this gives us better
		# control of error reporting.
		if ($rep->p_valid_config_entry($1, $2))
		{
		    if (($1 eq "filter") ||
			($1 eq "last_sync") ||
			($1 eq "new_sync") ||
			($1 eq "last_sync") ||
			($1 eq "peer") ||
			($1 eq "rename_to"))
		    {
			$rep->set_config($1, &p_clean_path("$data_dir/$2"));
		    }
		    else
		    {
			$rep->set_config($1, $2);
		    }
		}
		else
		{
		    $error = 1;
		    warn "$whoami: $file, line $. has is not recognized.\n";
		}
	    }
	    else
	    {
		$error = 1;
		warn "$whoami: $file, line $. has improper format.\n";
	    }
	}
	close(F);
    }
    if ($error)
    {
	die "$whoami: errors in config file $file; exiting\n";
    }
}

sub p_clean_path
{
    my @components = (split('/', $_[0]));
    my $i = 0;
    while ($i < scalar(@components))
    {
	if (($components[$i] eq "..") && ($i > 0))
	{
	    splice(@components, $i-1, 2);
	    $i--;
	}
	else
	{
	    $i++;
	}
    }
    join('/', @components);
}

# Public methods

sub other_umask
{
    my $rep = shift;
    $rep->{$package}{$f_other_umask} = $_[0] if @_ > 0;
    $rep->{$package}{$f_other_umask};
}

sub parse_samba
{
    my $rep = shift;
    my $samba_spec = shift;
    my $samba_config = 
    $rep->{$package}{$f_samba} = 1;
    $rep->{$package}{$f_samba_config} = new SambaSpec($samba_spec);
}

sub validate_config
{
    my $rep = shift;
    my $non_interactive = shift;
    my $gen_data = shift;

    $rep->p_compute_defaults();

    if ($rep->live())
    {
	$rep->p_validate_live();
    }
    else
    {
	$rep->p_compute_dbnames();
	if ($gen_data)
	{
	    $rep->p_validate_databases($non_interactive);
	}
    }

    if ($gen_data)
    {
	$rep->p_validate_filter();
	$rep->p_validate_umask();
    }
}

sub set_config
{
    my $rep = shift;
    my ($key, $val) = @_;

    my $whoami = $rep->p_whoami();
    my $config = $rep->p_config();

    $rep->p_valid_config_entry($key, $val) ||
	die "$whoami: set_config exiting\n";
    $config->{$key} = $val;
}

sub get_config
{
    my $rep = shift;
    my ($key) = @_;

    my $whoami = $rep->p_whoami();
    my $config = $rep->p_config();

    exists($config->{$key}) || die "$whoami: get_config: unknown key $key\n";
    $config->{$key};
}

sub bool_config
{
    my $rep = shift;
    my ($key) = @_;
    ($rep->get_config($key) eq "true");
}

sub actions
{
    $_[0]->{$package}{$f_actions};
}

sub tarlist
{
    $_[0]->{$package}{$f_tarlist};
}

sub olddata
{
    $_[0]->{$package}{$f_olddata};
}

sub diff
{
    $_[0]->{$package}{$f_diff};
}

sub conf_file
{
    $_[0]->{$package}{$f_conf_file};
}

sub pending
{
    if (ref($_[0]) ne "")
    {
	my $rep = shift;
	$rep->data_dir() . "/pending";
    }
    else
    {
	$_[0] . "/pending";
    }
}

sub conf_name
{
    $conf_name;
}

1;

#
# END OF QsyncConfig
#
