#!/usr/bin/env perl

#
# TO DO: add invalid response clauses to client dialog
#        check suspend operation
#

#
# fsync - a file mirroring utility
#
# Copyright (C) 1999-2002 Charles D. Schwieters
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    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., 675 Mass Ave, Cambridge, MA 02139, USA.
#

$^W=1;
$SIG{__WARN__} = warnHandler;
use strict;
use Cwd;
use Fcntl ':flock'; # import LOCK_* constants
use File::Copy;     # get smart move
use File::Find;
use File::Path;

#try to disable unicode behavior
if ( exists $ENV{'LANG'} && $ENV{'LANG'} ne "" ) {
    eval " no locale; ";
    eval " no utf8;   ";
    eval " use bytes; ";
    delete $ENV{'LANG'};
}
use File::Spec;

 my $versionStr = '$Id: fsync,v 2.112 2004/11/11 20:57:36 schwitrs Exp $';
my ($d1,$d2,$versionNum) = split(' ',$versionStr);
my $version = "fsync version $versionNum";


#
# synchronize files between remote and local machines
#

my $Slogfile      = $ENV{"HOME"} . "/.fsyncd.log";
my $tmpFilename   = ".fsyncd.$$.tmp";
my $verbose       = 0;
my $portDefault   = 9005;   # pick something not in use
my $port          = $portDefault;
my $rsh           = "";
my $mergeCmd     = "emacs --eval '(ediff \"Lfile\" \"Rfile\")'";
my $diffCmd      = "diff Lfile Rfile";
my $progname      = "bin/fsync";
my $cmd_file      = $ENV{"HOME"} . "/.fsyncrc";
my $rem_rootdir   = "";
my $remhost       = "";
my $proxyhost     = "";
my $proxyport     = "";
my $rootdir       = $ENV{"HOME"};
my $hashSaveFile  = ".fsync-hashsave";
my $rHashSaveFile = "";
my $hashMiss      = 0;
my $setDate       = 1;              #set file time to that of the other machine
my $setPerms      = 1;              #set file perms
my $setOwn        = 0;              #set uid/gid
my $delSpecs      = 0;              #delete remote files/dirs that are not local when mirroring
my $backupDir     = ".fsync-save";  #relative to $rootdir
my $rBackupDir    = "";             #defaults to $backupDir
my $promptFilesize= 1024*1024;      #for larger files, prompt before copy
my $copyBufferSize=100000;          #size (in bytes) of in-memory file buffer
my $clientRetries =50;              #connect retries client makes to server
my $stopServer    =1;               #stop server each time client stops
my $retryInterval =2;               #
my $dbSizeDelim   = 1;
my $dbBlockSize   = 100000;         # bytes
my $dbDelim       = '';             # RMAIL delimiter
my %dbFiles;
my $intCount      = 0;              # times cntrl-C has been pressed
my @excludeList   = ('^\.\.?$' , '^bin\.');
my @includeList   = ();
my $skipFilename  = ".fsync-skip";
my @skippedList   = ();
my $passcodeFilename  = ".fsync-passcode";
my $fsuidFilename     = ".fsync-fsuid";
my $followSymlinks= 0;
my $followTopLinks= 0;
my $omitHashEntry = 0;              # dont sync hash if true
my $diskspace_reserveSize = 10;     # in megabytes

my %spec;
my @currentKeys   = ();
my (%timeHashSave, %sizeHashSave, %csumHashSave);
my @crctab;                         #initialized in initCheckSum
my $checkSumProg = "cksum";
my $nethandle_in;
my $nethandle_out; 
my $pid=0;
my $skipFlag=0;
my $noErrorCheck=0;
my $errorMsg="";

#use IO::Socket;

if ($ARGV[0] eq '-s') {
    shift(@ARGV);
    Server(@ARGV);
} else {
    Client(@ARGV);
}

sub Usage {
    print STDERR "Usage: $0 [-l user] [-rh host] [-ph host] [-n] [-c] [-f file]\n";
    print STDERR "\t[-r rootdir] [-rr rem_rootdir] [-p port] [-pp port] [-h file] [-v[#]]\n";
    print STDERR "\t[-V] [file1 file2 ...]\n";
    print STDERR "\t-bd: name of the backup directory\n";
    print STDERR "\t-c:  copy all files which exist on only one machine\n";
    print STDERR "\t-cs: specify name of checksum program\n";
    print STDERR "\t-del: delete files when mirroring (-R or -L).\n";
    print STDERR "\t-f:  evaluate file as .rc file\n";
    print STDERR "\t-fs: follow symbolic links\n";
    print STDERR "\t-ft: follow top-level symbolic links\n";
    print STDERR "\t-hf: specify name of hash-save file\n";
    print STDERR "\t-l:  specify remote user name\n";
    print STDERR "\t-L:  mirror local files to remote\n";
    print STDERR "\t-m:  run merge command on all files which do not match\n";
    print STDERR "\t-n:  copy newer versions of all files\n";
    print STDERR "\t-p:  specify port for communication\n";
    print STDERR "\t-ph: specify a proxy host name\n";
    print STDERR "\t-pp: specify a proxy port\n";
    print STDERR "\t-r:  specify the root directory.\n";
    print STDERR "\t-R:  mirror remote files to local\n";
    print STDERR "\t-rh: specify remote machine name\n";
    print STDERR "\t-rr: specify the remote root directory.\n";
    print STDERR "\t-S:  skip all interactive prompts.\n";
    print STDERR "\t-nc: do not check for errors (faster)\n";
    print STDERR "\t-np: do not set permissions\n";
    print STDERR "\t-nd: do not set date/time\n";
    print STDERR "\t-ns: do not stop server when quitting client\n";
    print STDERR "\t-own: replicate uid/gid\n";
    print STDERR "\t-v[#]:  be verbose (verbose level- default: 1)\n";
    print STDERR "\t-V:  print version\n";
    print STDERR "\t--fast-sync-once:\n";
    print STDERR "\t\tno backups, checksum, hashSave file or error checking\n";

    print STDERR "Usage: $0 -s [-p port] [-v[#]] [-V]\n";
    print STDERR "\t-p:  specify port for communication\n";
    print STDERR "\t-v: be verbose.\n";
    print STDERR "\t-V:  print version\n";
    print STDERR "\tpasscode: sets the initial passcode expected\n";
    print STDERR "\t          by the server; if not set the server will accept\n";
    print STDERR "\t          the first connection without checking the password.\n";
}

##################################################
#
# client code
#
##################################################

sub Client {

    my ($cmd_remhost, $cmd_proxyhost, $cmd_proxyport, $remuser , 
	$cmd_rrootdir);
    my ($cmd_backupDir, $cmd_remuser, $cmd_checkSumProg, $cmd_hashSaveFile,
	$cmd_rootdir);
    my $copyFlag=0;
    my $mergeFlag=0;
    my $cmd_verbose=-1;
    my $newerFlag = 0;
    my $cmd_port = -1;;
    my $mirror = "";

    while (scalar(@_)>0 && $_[0] =~ /^-/) {
	if ($_[0] eq '--fast-sync-once') {
	    shift;
	    $cmd_checkSumProg = 'none';
	    $cmd_backupDir    = 'none';
	    $cmd_hashSaveFile = 'none';
	    $noErrorCheck     = 1;
	} elsif ($_[0] eq '-l') {
	    shift;
	    $remuser = shift;
	} elsif ($_[0] eq '-n') {
	    shift;
	    $newerFlag = 1;
	} elsif ($_[0] eq '-bd') {
	    shift;
	    $cmd_backupDir = shift;
	} elsif ($_[0] eq '-c') {
	    shift;
	    $copyFlag = 1;
	} elsif ($_[0] eq '-cs') {
	    shift;
	    $cmd_checkSumProg = shift;
	} elsif ($_[0] eq '-del') {
	    shift;
	    $delSpecs = $delSpecs?0:1;
	} elsif ($_[0] eq '-f') {
	    shift;
	    $cmd_file = shift;
	} elsif ($_[0] eq '-fs') {
	    shift;
	    $followSymlinks = 1;
	} elsif ($_[0] eq '-ft') {
	    shift;
	    $followTopLinks = 1;
	} elsif ($_[0] eq '-hf') {
	    shift;
	    $cmd_hashSaveFile = shift;
	} elsif ($_[0] eq '-L') {
	    shift;
	    $mirror="local";
	} elsif ($_[0] eq '-m') {
	    shift;
	    $mergeFlag = 1;
	} elsif ($_[0] eq '-r') {
	    shift;
	    $cmd_rootdir = shift;
	} elsif ($_[0] eq '-R') {
	    shift;
	    $mirror="remote";
	} elsif ($_[0] eq '-ph') {
	    shift;
	    $cmd_proxyhost = shift;
	} elsif ($_[0] eq '-pp') {
	    shift;
	    $cmd_proxyport = shift;
	} elsif ($_[0] eq '-p') {
	    shift;
	    $cmd_port = shift;
	} elsif ($_[0] eq '-rh') {
	    shift;
	    $cmd_remhost = shift;
	} elsif ($_[0] eq '-rr') {
	    shift;
	    $cmd_rrootdir = shift;
	} elsif ($_[0] eq '-S') {
	    shift;
	    $skipFlag = 1;
	} elsif ($_[0] =~ /^-v$/) {
	    shift;
	    $cmd_verbose = 1;
	} elsif ($_[0] =~ /^-v([0-9])/) {
	    shift;
	    $cmd_verbose = $1;
	} elsif ($_[0] =~ /^-V/) {
	    print "$version\n";
	    exit();
	} elsif ($_[0] eq '-h') {
	    Usage();
	    exit();
	} elsif ($_[0] eq '-nc') {
	    shift;
	    $noErrorCheck = $noErrorCheck?0:1;
	} elsif ($_[0] eq '-nd') {
	    shift;
	    $setDate = $setDate?0:1;
	} elsif ($_[0] eq '-np') {
	    shift;
	    $setPerms = $setPerms?0:1;
	} elsif ($_[0] eq '-ns') {
	    shift;
	    $stopServer = 0;
	} elsif ($_[0] eq '-own') {
	    shift;
	    $setOwn = $setOwn?0:1;
	} else {
	    Usage();
	    die "$0: invalid option.\n";
	}
    }
    
    my $default_skipFilename = $skipFilename;
    my $rem_skipFilename     = "";
    my @fileList;
    my @cmd_fileList = @_;
    ($cmd_verbose>=0) && ($verbose = $cmd_verbose);
    ($verbose>8) && print STDERR "command file list: @cmd_fileList\n";

    ($cmd_file =~ m!^/!) || ($cmd_file = getcwd() . "\/" . $cmd_file);
    if (-f $cmd_file) {
	undef($/);             # no input record separator
	open(CMDFILE,$cmd_file) || 
	    die "error opening command file: $cmd_file.\n";
	my $cmds = <CMDFILE>;  # reads whole file
	close(CMDFILE);
	$/ = "\n";             # reset input record separator
	eval($cmds);
	$@ eq "" || die "error parsing $cmd_file: $@";
    } else {
	print STDERR "does not exist: $cmd_file. Continuing...\n";
    }
    #
    # command-line overrides the rc file specification
    #
    $cmd_backupDir    && ($backupDir   = $cmd_backupDir);
    $cmd_remuser      && ($remuser     = $cmd_remuser);
    $cmd_checkSumProg && ($checkSumProg= $cmd_checkSumProg);
    $cmd_hashSaveFile && ($hashSaveFile= $cmd_hashSaveFile);
    $cmd_rootdir      && ($rootdir     = $cmd_rootdir);
    $cmd_rrootdir     && ($rem_rootdir = $cmd_rrootdir);
    ($cmd_verbose>=0) && ($verbose     = $cmd_verbose);
    $verbose && print STDERR "verbose: $verbose\n";

    #
    # command line path specification(s) override(s) the rc file
    #
    if (scalar(@cmd_fileList) > 0) {
	@fileList = ();
	my $pwd = getcwd();
	my ($dev,$rootDirInode) = stat($rootdir);
	foreach my $spec (@cmd_fileList) {
	    # make path absolute
	    ($spec !~ /^\// ) && ( $spec = $pwd . '/' . $spec );
	    $spec =~ s/\/\.$//;
	    my $prefix = $spec;
#	    $prefix =~ s/[^\/]*$//;
	    my $match=0;
	    ($verbose>7) && 
		print STDERR "locating rootdir in $spec:\n";
	    do {
		if ( Exists($prefix) ) {
		    ($verbose>7) && 
			print STDERR "trying $prefix...";
		    my ($dev,$inode) = stat($prefix);
		    if ( !$inode  && -l $prefix) {
			($dev,$inode) = lstat($prefix);
		    }
		    if ( $inode == $rootDirInode ) {
			$match=1;
			($verbose>7)&& print STDERR "MATCHED\n";
		    } else {
			($verbose>7)&& print STDERR "FAILED\n";
		    }
		}
	    } while ( (!$match) && $prefix =~ s/\/[^\/]*$//  );
	    if ( $match ) {
		$spec =~ s/^\Q$prefix\E//;
		$spec =~ s!^/!!g;   #remove leading slashes
		($spec eq "") && ( $spec = "." );
		($verbose>7)&& print STDERR "adding $spec to fileList\n";
		push( @fileList , $spec );
	    } else {
		print STDERR "WARNING: $spec not found in rootdir " .
		    "directory hierarchy. Skipping...\n";
	    }
	}
    }
    #FIX: remove O/S dependency
    #  remove trailing slashes
    for (my $i=0 ; $i<@fileList ; $i++) {
	$fileList[$i] =~ s!/$!!;
    }

    #
    #this entry added, so MakeHash doesn't recurse forever
    #
    my $dollar = "\$"; #so emacs auto-indent works...
    @excludeList = ( '\/\.\.?'.$dollar , @excludeList);
    for (my $i=0 ; $i<@excludeList ; $i++) {
	# remove exclude elements if file is explicitly specified.
	for my $file ( @fileList ) {
	    ($file =~ /$excludeList[$i]/) &&
		splice(@excludeList,$i,1);
	}
    }
    if ($verbose > 1) {
	print STDERR "exclude list:\n";
	foreach my $elem (@excludeList) {
	    print STDERR "\t$elem\n";
	}
    }

    #
    $mirror && ( $skipFlag=1 );
    $mirror && ( $copyFlag=1 );
    ($verbose>5) && printf STDERR "setDate:   %d\n",$setDate;
    ($verbose>5) && printf STDERR "setOwn:    %d\n",$setOwn;
    ($verbose>5) && printf STDERR "copyFlag:  %d\n",$copyFlag;
    ($verbose>5) && printf STDERR "mergeFlag: %d\n",$mergeFlag;
    ($verbose>5) && printf STDERR "newerFlag: %d\n",$newerFlag;
    ($verbose>5) && printf STDERR "skipFlag:  %d\n",$skipFlag;
    ($verbose>5) && printf STDERR "mirror:    %s\n",$mirror;

    my $passcode = int(2**32 * abs(rand()));

    #
    # install signal handler
    #
    $SIG{'PIPE'} = $SIG{'INT'} = $SIG{'QUIT'} = 'clientHandler';
    $SIG{'HUP'} = 'IGNORE';

    #
    # start server
    #
    $cmd_remhost      && ($remhost     = $cmd_remhost);
    ($cmd_port>=0)    && ($port        = $cmd_port);
    $cmd_proxyhost    && ($proxyhost   = $cmd_proxyhost);
    $cmd_proxyport    && ($proxyport   = $cmd_proxyport);
    my $proxyequalsremote = !$proxyhost && !$proxyport;
    $proxyhost || ($proxyhost = $remhost);
    $proxyport || ($proxyport = $port);
    
    if (-f $passcodeFilename) {
	open(pcFILE,"$passcodeFilename") || 
	    die("error opening $passcodeFilename");
	my $line = <pcFILE>;
	my ($lremhost, $lport, $lproxyhost, $lproxyport);
	($passcode, $lremhost, $lport, $lproxyhost, $lproxyport) = 
	    split(" ",$line);
	close(pcFILE);
	if ( $remhost eq $lremhost &&
	     $port    == $lport     ) {
	    $proxyhost = $lproxyhost;
	    $proxyport = $lproxyport;
	    unlink($passcodeFilename);
	    $verbose && 
		print STDERR "trying to contact server at $remhost:$port " .
		    "via $proxyhost:$proxyport...\n";
	    $nethandle_in = contactServer(\$nethandle_out,$port,
					  $proxyhost,$proxyport,1);
	}
    } 

    #try to contact a running server
    $nethandle_in || ($nethandle_in = contactServer(\$nethandle_out,$port,
				      $proxyhost,$proxyport,0));
    if ( !$nethandle_in ) {
	spawnServer($rsh,$proxyequalsremote,$remuser,$passcode);
	$nethandle_in = contactServer(\$nethandle_out,$port,
				      $proxyhost,$proxyport,$clientRetries);
    }
    if ( !$nethandle_in ) {
	print STDERR "Failed to contact server. Exiting.\n";
	exit 1;
    }

    ($verbose>2) && print STDERR "sending passcode\n";
    sendLine($passcode);
	     
    sendLine("version");
    my $RversionNum = getLine();
    $verbose>1 && print STDERR "server version is $RversionNum\n";
    if ($versionNum != $RversionNum) {
        print STDERR "Warning! The server version of fsync does not match.\n";
        print STDERR "\tlocal version: $versionNum" .
	    "\tremote version: $RversionNum\n";
    }
    my ($major, $minor) = split(/\./,$RversionNum);
    if ( ($major == 2 && $minor < 107) || $major < 2) {
        print STDERR 
	    "Fatal error: " .
		"remote server is running an incompatible version of fsync: " .
		    "($major.$minor)\n";
        exit(2);
    }

    sendCmd("follow-symlinks $followSymlinks");
    sendCmd("follow-toplinks $followTopLinks");

    #FIX: does this belong here?
    # resync stdin: the forked process may have mucked with it.
    # OS dependency!
    $skipFlag ||
	open(STDIN,"/dev/tty") || die "couldn't open stdin!\n";

    #
    # set the name of the checksum program on the server
    #
    initCheckSum();
    sendCmd("checksum-prog",$checkSumProg);

    #
    # set the root directory on the server
    #
    length($rem_rootdir) && sendCmd("rootdir","$rem_rootdir");
    #
    # check that there's enough disk space in rootdir, rem_rootdir
    #
    sendLine("check-diskspace $diskspace_reserveSize");
    my $low=0;
    if ( !checkDiskSpace($diskspace_reserveSize) ) {
	print STDERR "WARNING\n";
	print STDERR "WARNING: less than $diskspace_reserveSize mb " .
	    "available locally\n";
	print STDERR "WARNING\n";
	$low=1;
    } 
    my $ret=getLine();
    if ( $ret !~ /^ok/ ) {
	print STDERR "WARNING\n";
	print STDERR "WARNING: less than $diskspace_reserveSize mb " .
	    "available remotely\n";
	print STDERR "WARNING\n";
	$low=1;
    } 
    if ( $low ) {
	print "Continue?\n";
	my $ans=
	    readInteractive("y:n","n",
			    "you are low on disk space. If you run out of",
			    "space during an fsync session files can be",
			    "truncated.",
			    "To avoid this prompt, modify ~/.fsyncrc",
			    "(y)  continue",
			    "(n)  abort fync to fix problem");
	if ( $ans !~ /^y/ ) {
	    ClientQuit();
	}
    }
    
    #
    # set the name of the backup directory on the server
    #
    length($rBackupDir) || length($backupDir) && 
	($rBackupDir = $backupDir);
    length($rBackupDir) &&
	sendCmd("backup-dir",$rBackupDir);
    #
    # set the name of the hashSaveFile on the server
    #
    length($rHashSaveFile) || length($hashSaveFile) && 
	($rHashSaveFile = $hashSaveFile);
    length($rHashSaveFile) &&
	sendCmd("hash-save-file",$rHashSaveFile);
    #
    $verbose && print STDERR "rootdir: $rootdir\n";
    chdir($rootdir) || die "chdir to $rootdir failed!\n";
    #
    sendLine("file-list " . scalar(@fileList)); sendArray( \@fileList );
    ($verbose>1) && print STDERR "files to sync:\n";
    foreach my $file ( @fileList ) {
	($verbose>1) && print STDERR "\t$file\n";
    }
    $rem_skipFilename eq "" && ( $rem_skipFilename = $skipFilename );
    $default_skipFilename ne $rem_skipFilename &&
	sendCmd("skip-file",$rem_skipFilename);
	
 

    #
    # Begin sync loop
    #
    $setDate  || sendCmd("set-date");
    $setPerms || sendCmd("set-perms");
    $setOwn   && sendCmd("set-own");

    if ( $hashSaveFile =~ /FSUID/ ||
	 $rHashSaveFile =~ /FSUID/ ) {
	my $fsuid = readFsuid();
	sendLine("fsuid $fsuid");
	my $rem_fsuid = getLine();
	$fsuid eq $rem_fsuid &&
	    print STDERR "WARNING WARNING WARNING: "
		. "fsuid($fsuid) == rem_fsuid($rem_fsuid)\n:";
	$hashSaveFile =~ s/FSUID/$rem_fsuid/g;
    }

    #
    # send exclude list
    #
    sendLine("make-hash " . scalar(@excludeList));
    sendArray( \@excludeList );
    
    #
    # generate local hashes
    #
    $verbose && print STDERR "generating hashes locally...\n";
    MakeHashes(@fileList);
    #
    # get hashes from server
    #
    $verbose && print STDERR "receiving hashes from $remhost...\n";
    sendLine("get-hash");
    my %Rspec;
    my @RskippedList = ();
    getHash2D(\%{$Rspec{"file"}});
    getHash2D(\%{$Rspec{"symlink"}});
    NgetHash(\%{$Rspec{"hardlink"}});
    NgetHash(\%{$Rspec{"dir"}});
    my $len = getLine();
    getArray(\@RskippedList,$len);
    
    
    if ($verbose>3) {
	print STDERR "contents of remote spec:\n";
	printHash(\%Rspec,"file");
	printHash(\%Rspec,"dir");
	printHash(\%Rspec,"symlink");
	printHash(\%Rspec,"hardlink");
	print STDERR "contents of local spec:\n";
	printHash(\%spec,"file");
	printHash(\%spec,"dir");
	printHash(\%spec,"symlink");
	printHash(\%spec,"hardlink");
    }
    
#
#add: make sure that both sets of hashes have all entries - for some field
#   - so that copyFrom updates the hashes
#

 	 #
 	 # Compare hashes
 	 #
 	 
 	 # if file not present:
 	 #  1) if dir, create it
 	 #  2) if regular file, copy it
 	 #  3) if symlink create it
 	 # 

    #
    # process skipped list- make sure they're the same
    # 
    $verbose>5 && print STDERR "skippedList: \n";
    foreach my $spec ( @skippedList ) {
	$verbose>5 && print STDERR "\t$spec\n";
	my $present=0;
	foreach my $Rspec ( @RskippedList ) {
	    ($spec eq $Rspec) && ($present=1);
	}
	if (!$present) {
	    $verbose>1 && print STDERR "Warning : $spec\n" .
		"\tis ignored due to a .fsync-skip entry locally, but not " .
		    "on $remhost... skipping.\n";
	    RemoveDirEntries(\%Rspec,$spec);
	}
    }
    $verbose>5 && print STDERR "RskippedList: \n";
    foreach my $Rspec ( @RskippedList ) {
	$verbose>5 && print STDERR "\t$Rspec\n";
	my $present=0;
	foreach my $spec ( @skippedList ) {
	    ($spec eq $Rspec) && ($present=1);
	}
	if (!$present) {
	    $verbose>1 && print STDERR "Warning : $Rspec\n" .
		"\tis ignored due to a .fsync-skip entry on $remhost, " .
		    "but not locally... skipping.\n";
	    RemoveDirEntries(\%spec,$Rspec);
	}
    }

    #
    # see if the separate types of hashes have common entries.
    #
    if ( $mirror eq "local" ) {
	# 2) find all members of Rspec which are not in the correct field of
	# spec and delete them.
	foreach my $type (keys %Rspec) {
	    foreach my $spec (keys %{$Rspec{$type}}) {
		if ( !exists $spec{$type}{$spec} ) {
		    $delSpecs && sendCmd("delete",$spec);
		    delete $Rspec{$type}{$spec};
		}
	    }
	}
	# FIX: deal with hardlinks
    } elsif ( $mirror eq "remote" ) {
	# 2) find all members of spec which are not in the correct field of
	# Rspec and delete them.
	foreach my $type (keys %spec) {
	    foreach my $spec (keys %{$spec{$type}}) {
		if ( !exists $Rspec{$type}{$spec} ) {
		    $delSpecs && Delete($spec);
		    delete $spec{$type}{$spec};
		}
	    }
	}
	# FIX: deal with hardlinks
    } else { ## not mirroring
	deleteHashCollisions(\%spec,\%Rspec,"file","dir");
#	deleteHashCollisions(\%spec,\%Rspec,"symlink","dir");
	deleteHashCollisions(\%spec,\%Rspec,"hardlink","dir");

## if there is an entry in fileHash, then either:
## a) the first link doesn't exist , or
## b) the files are separate and non-linked
##
	foreach my $link (keys %{$spec{"hardlink"}} ) {
	    if (exists $Rspec{"file"}{$link} &&
		exists $Rspec{"file"}{$spec{"hardlink"}{$link}}) {
		
		print STDERR "the names $link and " . $spec{"hardlink"}{$link} .
		    " are links to the same file locally\n";
		print STDERR "\tbut separate files on $remhost. Skipping...\n";
		delete $Rspec{"file"}{$link};
		delete $spec{"hardlink"}{$link};
	    }
	    if (exists $Rspec{"dir"}{$link}) {
		print STDERR "the name $link is a hardlink locally\n";
		print STDERR "\tbut a directory on $remhost. Skipping...\n";
		delete $Rspec{"dir"}{$link};
		delete $spec{"hardlink"}{$link};
		RemoveDirEntries(\%Rspec,$link);
	    }
	}
	# if single file on one side and hard link on other,
	# must make the file be the same on each side
      swapLoop1:
	foreach my $link (keys %{$Rspec{"hardlink"}} ) {
	    if (exists $spec{"file"}{$link}) {
		print STDERR 
		    "file(loc)/hardlink(rem) collision for $link:\n\t";
		my $too=0;
		foreach my $link2 (keys %{$Rspec{"hardlink"}} ) {
		    if ( $link ne $link2 &&
			 $Rspec{"hardlink"}{$link} eq
			 $Rspec{"hardlink"}{$link2} ) {
			print STDERR " >1 hard link. Skipping...\n";
			delete $Rspec{"hardlink"}{$link2};
			$too=1;
		    }
		}
		if ($too) {
		    delete $Rspec{"hardlink"}{$link};
		    delete $spec{"file"}{$link};
		    next swapLoop1;
		}
		$verbose && print STDERR \
		    "swapping $link and " . $Rspec{"hardlink"}{$link} . "\n";
		$Rspec{"file"}{$link} = $Rspec{"file"}{$Rspec{"hardlink"}{$link}};
		$Rspec{"hardlink"}{ $Rspec{"hardlink"}{$link} } = $link;
		delete $Rspec{"file"}{ $Rspec{"hardlink"}{$link} };
		delete $Rspec{"hardlink"}{ $link };
	    }
	}
	my @keys2 = (keys %{$spec{"hardlink"}} );
	while ( scalar( @keys2 ) ) {
	    my $link = shift( @keys2);
	    if (exists $spec{"hardlink"}{$link} && 
		exists $Rspec{"file"}{$link}) {
		print STDERR 
		    "file(rem)/hardlink(loc) collision for $link:\n\t";
		my $too=0;
		foreach my $link2 ( @keys2 ) {
		    if ( $spec{"hardlink"}{$link} eq
			 $spec{"hardlink"}{$link2} ) {
			print STDERR " >1 hard link. Skipping...\n";
			delete $spec{"hardlink"}{$link2};
			$too=1;
		    }
		}
		if ($too) {
		    delete $spec{"hardlink"}{$link};
		    delete $Rspec{"file"}{$link};
		} else {
		    $verbose&&print STDERR "swapping $link and " . 
			$spec{"hardlink"}{$link} . "\n";
		    $spec{"file"}{$link} = 
			$spec{"file"}{ $spec{"hardlink"}{$link} };
		    $spec{"hardlink"}{ $spec{"hardlink"}{$link} } = $link;
		    delete $spec{"file"}{ $spec{"hardlink"}{$link} };
		    delete $spec{"hardlink"}{ $link };
		}
	    }
	}
    } 
	    

    #
    # compare directories
    #
    $verbose && print STDERR "Processing directories...\n";
    my @dirExcludeList;
  remoteDirLoop:
    foreach my $dir (sort keys(%{$Rspec{"dir"}})) {
	if (exists $spec{"dir"}{$dir}) {
	    syncHash($dir);
	    sendLine("sync-hash"); sendLine($dir);
	    delete $spec{"dir"}{$dir};
	} else {
	    # exclude all directories which are subdirectories of 
	    # directories we have chosen to not make
	    for my $excludedDirs ( @dirExcludeList ) {
		if ( $dir =~ /^\Q$excludedDirs\E\// ) { #FIX: OS dependency
		    next remoteDirLoop;
		}
	    }
	    my $ans = 'c';
	    print "directory $dir exists on $remhost, " .
		"but not locally.\n";
	    if ( exists $timeHashSave{$dir} ) {
		print "\t*** has been deleted ***\n";
		$copyFlag && $delSpecs && ($mirror ne "remote") && 
		    ( $ans = "del" );
	    }
	    if ( !$copyFlag ) {
		$ans = 
		    readWithQuit("c:del:ign:IGN:R:s","s",
				    "(c)   create directory locally" ,
				    "(del) delete remote directory" ,
				    "(ign) ignore permanently: add to " .
				    "excludeList in $cmd_file",
				    "(IGN) ignore permanently: add to " .
				    "$skipFilename",
				    "(R)   rename",
				    "(s)   skip");
	    }
	    if ($ans =~ /^c/i) {
		$verbose && 
		    print STDERR "creating directory $dir locally...\n";
		if ( $setOwn ) {
		    sendLine("get-own");
		    sendLine($dir);
		    my $uid= getLine();
		    my $gid= getLine();
		    checkError();
		    makedir($dir,$Rspec{"dir"}{$dir},$uid,$gid);
		} else {
		    makedir($dir,$Rspec{"dir"}{$dir});
		}
		syncHash($dir);
		sendLine("sync-hash"); sendLine($dir);
	    } elsif ($ans =~ /^del/i) {
		$verbose && 
		    print STDERR "deleting directory $dir on $remhost...\n";
		sendCmd("delete",$dir);
		Delete($dir);
	    } elsif ($ans =~ /^ign/) {
		ignoreSpec($dir,"dir",\%spec,\%Rspec,"rc");
	    } elsif ($ans =~ /^IGN/) {
		ignoreSpec($dir,"dir",\%spec,\%Rspec,"skip");
	    } elsif ($ans =~ /^R/) {
		my @words = split(" ",$ans);
		my $newName;
		if ( @words>1 ) {
		    $newName = $words[1];
		} else {
		    print "new name: ";
		    $newName = readFlushedStdin();
		}
		$newName ne '' &&
		    sendCmd( ("rename",$dir,$newName) );
	    }
	    if ( $ans =~ /^del/i ||
		 $ans =~ /^ign/i ||
		 $ans =~ /^R/    ||
		 $ans =~ /^s/i     ) {
		push(@dirExcludeList,$dir);
		RemoveDirEntries(\%Rspec,$dir);
	    }
	}
    }
  localDirLoop:
    foreach my $dir (sort keys( %{$spec{"dir"}} )) {
	($verbose>5) && print STDERR "processing: $dir\n";
	# exclude all directories which are subdirectories of 
	# directories we have chosen to not make
	for my $excludedDirs ( @dirExcludeList ) {
	    if ( $dir =~ /^\Q$excludedDirs\E\// ) { #FIX: OS dependency
		next localDirLoop;
	    }
	}
	sendLine("in-hash-save");
	sendLine($dir);
	my $deleted = getLine();
	my $ans = 'c';
	print "directory $dir exists locally, " . 
	    "but not on $remhost.\n";
	if ( $deleted ) {
	    print "\t*** has been deleted ***\n";
	    $copyFlag && $delSpecs && ($mirror ne "local") && 
		( $ans = "del" );
	}
	if ( !$copyFlag ) {
	    $ans = 
		readWithQuit("c:del:ign:IGN:R:s","s",
			     "(c)   create directory on $remhost" ,
			     "(del) delete directory on $remhost" ,
			     "(ign) ignore permanently: add to " .
			     "excludeList in $cmd_file",
			     "(IGN) ignore permanently: add to " .
			     "$skipFilename",
			     "(R)   rename",
			     "(s)   skip");
	}
	if ($ans =~ /^c/i) {
	    $verbose && 
		print STDERR 
		    "creating directory $dir on $remhost...\n";
	    my $cmd = "mkdir " . $spec{"dir"}{$dir};
	    if ( $setOwn ) {
		my ($dev,$ino,$mode,$nlink,$uid,$gid) = stat($dir);
		$cmd .= " $uid $gid";
	    }
	    sendCmd($cmd,$dir);
	    syncHash($dir);
	    sendLine("sync-hash"); sendLine($dir);
	} elsif ($ans =~ /^del/i) {
	    $verbose && 
		print STDERR "deleting local directory $dir...\n";
	    sendCmd("delete",$dir);
	    Delete($dir);
	} elsif ($ans =~ /^ign/) {
	    ignoreSpec($dir,"dir",\%spec,\%Rspec,"rc");
	} elsif ($ans =~ /^IGN/) {
	    ignoreSpec($dir,"dir",\%spec,\%Rspec,"skip");
	} elsif ($ans =~ /^R/) {
	    my @words = split(" ",$ans);
	    my $newName;
	    if ( @words>1 ) {
		$newName = $words[1];
	    } else {
		print "new name: ";
		$newName = readFlushedStdin();
	    }
	    $newName ne '' && fsyncRename($dir,$newName);
	} 
	if ( $ans =~ /^del/i ||
	     $ans =~ /^ign/i ||
	     $ans =~ /^R/    ||
	     $ans =~ /^s/i     ) {
	    push(@dirExcludeList,$dir);
	    RemoveDirEntries(\%spec,$dir);
	}
    }

    #
    # compare symlinks
    #
    $verbose && print STDERR "Processing symbolic links...\n";
    # check for existance
    
    foreach my $link (sort keys( %{$Rspec{"symlink"}} )) {
	if (!exists $spec{"symlink"}{$link}) {
	    print "symlink $link --> " .
		$Rspec{"symlink"}{$link}{"target"} . 
		    "\n\texists on $remhost, but not locally.\n";
	    my $ans = 'c';
	    if ( !$copyFlag ) {
		$ans = readWithQuit("c:del:ign:IGN:R:s",'s',
				    "(c)   create the symlink",
				    "(del) delete the symlink",
				    "(ign) ignore permanently: add to " .
				    "excludeList in $cmd_file",
				    "(IGN) ignore permanently: add to " .
				    "$skipFilename",
				    "(R)   rename",
				    "(s)   skip");
	    }
	    if    ($ans =~ /^c/i)   { 
		$verbose && print STDERR "creating $link locally...\n";
		makeSymlink($Rspec{"symlink"}{$link}{"target"} , $link); }
	    elsif ($ans =~ /^del/i) { 
		sendCmd("delete",$link);
		Delete($link);
	    } elsif ($ans =~ /^ign/) { 
		ignoreSpec($link,"symlink",\%spec,\%Rspec,"rc");
	    } elsif ($ans =~ /^IGN/) {
		ignoreSpec($link,"symlink",\%spec,\%Rspec,"skip");
	    } elsif ($ans =~ /^R/) {
		my @words = split(" ",$ans);
		my $newName;
		if ( @words>1 ) {
		    $newName = $words[1];
		} else {
		    print "new name: ";
		    $newName = readFlushedStdin();
		}
		$newName ne '' &&
		    sendCmd( ("rename", $link, $newName) );
	    } 
	} else {
	    # check that the links match
	    my $sizeEqual = $spec{"symlink"}{$link}{"size"} == 
		$Rspec{"symlink"}{$link}{"size"};
	    my $csumEqual = $spec{"symlink"}{$link}{"csum"} == 
		$Rspec{"symlink"}{$link}{"csum"};
	    my $timeEqual = $spec{"symlink"}{$link}{"time"} == 
		$Rspec{"symlink"}{$link}{"time"};
		
	    if ( !$csumEqual )   {
		my $ok=0;
		my $ans='';
		$newerFlag && ( $ans = 'n' );
		$mirror    && ( $ans = $mirror );
		($ans eq '') && $mergeFlag && ( $ans='m' );
		print "symlink $link differs.\n";
		print " local : ";
		if ($spec{"symlink"}{$link}{"time"} >
		    $Rspec{"symlink"}{$link}{"time"} ) {
		    print "**newer**";
		} else { print "         ";}
		print " " . localtime($spec{"symlink"}{$link}{"time"}) .
		    " --> " . $spec{"symlink"}{$link}{"target"};
		$spec{"symlink"}{$link}{"mod"} && 
			print " **modified**";
		print "\n";
		print " remote: ";
		if ($spec{"symlink"}{$link}{"time"} <
		    $Rspec{"symlink"}{$link}{"time"} ) {
		    print "**newer**";
		} else { print "         ";}
		print " " . localtime($Rspec{"symlink"}{$link}{"time"}) . 
		    " --> " . $Rspec{"symlink"}{$link}{"target"};
		$Rspec{"symlink"}{$link}{"mod"} && 
			print " **modified**";
		print "\n";


		while ($ok==0) {
		    if ($ans eq '') { #FIX: want to print action
			$ans = 
			    readWithQuit("l:r:n:del:ign:IGN:m:d:s","s",
					 "(l) copy local to remote",
					 "(r) copy remote to local",
					 "(n) copy newer version",
					 "(del) delete both versions",
					 "(ign) ignore permanently: add to " .
					 "excludeList in $cmd_file",
					 "(IGN) ignore permanently: add to " .
					 "$skipFilename",
					 "(s) skip this symlink");
		    }
		    $ok=1;
		    if    ($ans =~ /^l/i) { 
		      $verbose && print STDERR "using local version.\n";
		     sendCmd("symlink",$spec{"symlink"}{$link}{"target"},$link);
		    }elsif ($ans =~ /^r/i) { 
		      $verbose && print STDERR "using remote version.\n";
		      Delete($link);
		      makeSymlink($Rspec{"symlink"}{$link}{"target"},$link);
		    } elsif ($ans =~ /^m/i) { MergeFile($link); }
		    elsif ($ans =~ /^n/i) {
			if ($spec{"symlink"}{$link}{"time"} < 
			    $Rspec{"symlink"}{$link}{"time"}  &&
			    !$spec{"symlink"}{$link}{"mod"}) {
			    Delete($link);
			    makeSymlink($Rspec{"symlink"}{$link}{"target"},
					$link);
			} elsif ($spec{"symlink"}{$link}{"time"} > 
				 $Rspec{"symlink"}{$link}{"time"} &&
				 !$Rspec{"symlink"}{$link}{"mod"}) {
			    sendCmd("symlink",$spec{"symlink"}{$link}{"target"},$link);
			} else {
			    print 
				"both versions modified. Specify version.\n";
			    $ans='';
			    $ok=0;
			} }
		    elsif ($ans =~ /^s/i) {  }
		    elsif ($ans =~ /^del/i) {
			$verbose && print STDERR "deleting both versions.\n";
			Delete($link);
			sendCmd("delete",$link);
		    } elsif ($ans =~ /^ign/) {
			ignoreSpec($link,"symlink",\%Rspec,\%spec,"rc");
		    } elsif ($ans =~ /^IGN/) {
			ignoreSpec($link,"symlink",\%Rspec,\%spec,"skip");
		    } 
		    else  { print "invalid response.\n"; $ans='';$ok=0;}
		}
		checkError();
	    }
	}
	$spec{"symlink"}{$link}{"mod"} &&  syncHash($link);
	if ( $Rspec{"symlink"}{$link}{"mod"} ) {
	    sendLine("sync-hash");
	    sendLine($link);
	}
	delete $Rspec{"symlink"}{$link};
	delete $spec{"symlink"}{$link};
    }
    foreach my $link (sort keys( %{$spec{"symlink"}} )) {
	if (!exists $Rspec{"symlink"}{$link}) {
	    print "symlink $link --> " .
		$spec{"symlink"}{$link}{"target"} . 
		    "\n\texists locally, but not on $remhost.\n";
	    my $ans='c';
	    $copyFlag ||
		( $ans = readWithQuit("c:del:ign:IGN:R:s",'s',
				      "(c)   create the symlink",
				      "(del) delete the symlink",
				      "(ign) ignore permanently: add to " .
				      "excludeList in $cmd_file",
				      "(IGN) ignore permanently: add to " .
				      "$skipFilename",
				      "(R)   rename",
				      "(s)   skip") );
	    if ($ans =~ /^c/i)   { 
		$verbose && print STDERR "creating $link on $remhost...\n";
		sendCmd("symlink",$spec{"symlink"}{$link}{"target"},$link); }
	    elsif ($ans =~ /^del/i) { 
		sendCmd("delete",$link);
		Delete($link);
	    } elsif ($ans =~ /^ign/) {
		ignoreSpec($link,"symlink",\%spec,\%Rspec,"rc");
	    } elsif ($ans =~ /^IGN/) {
		ignoreSpec($link,"symlink",\%spec,\%Rspec,"skip");
	    } elsif ($ans =~ /^R/) {
		my @words = split(" ",$ans);
		my $newName;
		if ( @words>1 ) {
		    $newName = $words[1];
		} else {
		    print "new name: ";
		    $newName = readFlushedStdin();
		}
		$newName eq '' || fsyncRename($link,$newName);
	    } 
	}
    }

    #
    # compare files
    #
    $verbose && print STDERR "Processing files...\n";
    # check for existance

    @currentKeys = sort(keys( %{$Rspec{"file"}}));
#what if member is deleted from spec, but not from currentKeys?
#
#what if member is deleted from currentKeys, but not from spec?
#
    while ( @currentKeys ) {
	my $file = shift( @currentKeys );
	($verbose>1) && print STDERR "Processing file $file\n";
	if (!exists $spec{"file"}{$file}{"size"}) {
	    my $ok=0;
	    my $ans='c';
	    while ( !$ok ) {
		$ok=1;
		print "file $file: " .
		    "(" . $Rspec{"file"}{$file}{"size"} . " bytes) " .
			localtime($Rspec{"file"}{$file}{"time"}) .
			    "\n\texists on $remhost, but not locally.\n";
		if ( exists $timeHashSave{$file} ) {
		    print "\t*** has been deleted ***\n";
		    $copyFlag && $delSpecs && ($mirror ne "remote") &&
			( $ans = "del" );
		}
		if ( !$copyFlag ) {
		    $ans = readWithQuit("c:del:ign:IGN:R:s","s",
					"(c)   copy the file",
					"(del) delete the file",
					"(ign) ignore permanently: add to " .
					"excludeList in $cmd_file",
					"(IGN) ignore permanently: add to " .
					"$skipFilename",
					"(R)   rename",
					"(s)   skip the file");
		}
		if    ($ans =~ /^c/i)   { clientCopyFrom($file,$file); }
		elsif ($ans =~ /^del/i) { 
		    sendCmd("delete",$file);
		    Delete($file);
		} elsif ($ans =~ /^s/) {
		} elsif ($ans =~ /^ign/) {
		    ignoreSpec($file,"file",\%Rspec,\%spec,"rc");
		} elsif ($ans =~ /^IGN/) {
		    ignoreSpec($file,"file",\%Rspec,\%spec,"skip");
		} elsif ($ans =~ /^R/) {
		    my @words = split(" ",$ans);
		    my $newName;
		    if ( @words>1 ) {
			$newName = $words[1];
		    } else {
			print "new name: ";
			$newName = readFlushedStdin();
		    }
		    $newName ne '' && 
			sendCmd( ("rename", $file, $newName) );
		}
		else {
		    print "invalid response.\n"; 
		    $ans='';$ok=0;
		}
	    }
	} else {
	    # check size/checksum
	    #
	    #  a CheckSum is not created is a file is older than the
	    #  the time stamp. Thus, if both files are older than the
	    #  time stamp, and have the same size, but differ, the 
	    #  current logic will still assume that they are the same.
	    #
	    my $sizeEqual = 
		$spec{"file"}{$file}{"size"} == $Rspec{"file"}{$file}{"size"};
	    my $csumEqual = 
		$spec{"file"}{$file}{"csum"} == $Rspec{"file"}{$file}{"csum"};
	    my $timeEqual = 
		$spec{"file"}{$file}{"time"} == $Rspec{"file"}{$file}{"time"};
		
	    if ( !$sizeEqual || !$csumEqual ||
		($setDate && !$timeEqual) )   {
		my $ok=0;
		my $ans='';
		$newerFlag && ( $ans = 'n' );
		$mirror    && ( $ans = $mirror );
		($ans eq '') && $mergeFlag && ( $ans='m' );
#
# add: increase -n intelligence (to make it safer) 
#   if one copy is different, check its timestamp. 
#   Need extra bit of info: did the hashSave info differ from the data
#   on disk? If so, the file is marked as modified.
#   If both files are modified, a merge is forced.
#   If one file is modified and the other has a timestamp which
#   is newer than the timestamp in the modified copy's hashSave,
#   a merge is forced.
#
		print "file $file differs.\n";
		print "     local : (" . $spec{"file"}{$file}{"size"} . 
		    " bytes) " . localtime($spec{"file"}{$file}{"time"});
		if ($spec{"file"}{$file}{"time"} > 
		    $Rspec{"file"}{$file}{"time"} ) {
		    print " **newer**";
		} else { print "          ";}
		$spec{"file"}{$file}{"mod"} && 
		    print " **modified**";
		print "\n";
		print "     remote: (" .$Rspec{"file"}{$file}{"size"}.
		    " bytes) " .
			localtime($Rspec{"file"}{$file}{"time"});
		if ($Rspec{"file"}{$file}{"time"} > 
		    $spec{"file"}{$file}{"time"} ) {
		    print " **newer**";
		} else { print "          ";}
		$Rspec{"file"}{$file}{"mod"} && 
		    print " **modified**";
		print "\n";
		while ($ok==0) {
		    if ($ans eq '') { #FIX: want to print action
			$ans = 
			    readWithQuit("l:r:n:del:ign:IGN:m:d:s","s",
					 "(l) copy local to remote",
					 "(r) copy remote to local",
					 "(n) copy newer version",
					 "(del) delete both versions",
					 "(ign) ignore permanently: add to " .
					 "excludeList in $cmd_file",
					 "(IGN) ignore permanently: add to " .
					 "$skipFilename",
					 "(m) merge changes",
					 "(d) run diffn",
					 "(s) skip this file");
		    }
		    $ok=1;
		    if    ($ans =~ /^l/i) { 
			dbClientCopyTo($file,$sizeEqual&&$csumEqual);
		    }elsif ($ans =~ /^r/i) { 
			dbClientCopyFrom($file,$sizeEqual&&$csumEqual); 
		    } elsif ($ans =~ /^m/i) { MergeFile($file); }
		    elsif ($ans =~ /^n/i) {
			if ($spec{"file"}{$file}{"time"} < 
			    $Rspec{"file"}{$file}{"time"}  &&
			    !$spec{"file"}{$file}{"mod"}) {
			    dbClientCopyFrom($file,$sizeEqual&&$csumEqual);
			} elsif ($spec{"file"}{$file}{"time"} > 
				 $Rspec{"file"}{$file}{"time"} &&
				 !$Rspec{"file"}{$file}{"mod"}) {
			    dbClientCopyTo($file,$sizeEqual&&$csumEqual);
			} else {
			    print 
				"both versions modified. Merge suggested.\n";
			    $ans='';
			    $ok=0;
			} }
		    elsif ($ans =~ /^s/i) {  }
		    elsif ($ans =~ /^del/i) {
			Delete($file);
			sendCmd("delete",$file);
		    } elsif ($ans =~ /^ign/) {
			ignoreSpec($file,"file",\%Rspec,\%spec,"rc");
		    } elsif ($ans =~ /^IGN/) {
			ignoreSpec($file,"file",\%Rspec,\%spec,"skip");
		    } 
		    elsif ($ans =~ /^d/i) { DiffFile($file);$ans='';$ok=0;}
		    else  { print "invalid response.\n"; $ans='';$ok=0;}
		}
		checkError();
	    } else { # fix up false mods
		$spec{"file"}{$file}{"mod"} &&
		    syncHash($file);
		if ( $Rspec{"file"}{$file}{"mod"} ) {
		    sendLine("sync-hash");
		    sendLine($file);
		}
	    }
	}
	delete $Rspec{"file"}{$file};
	delete $spec{"file"}{$file};
    }
    @currentKeys = sort(keys( %{$spec{"file"}}));
    while ( @currentKeys ) {
	my $file = shift( @currentKeys );
	if (!exists $Rspec{"file"}{$file}{"size"}) {
	    sendLine("in-hash-save");
	    sendLine($file);
	    my $deleted = getLine();
	    my $ans='c';
	    my $ok=0;
	    while ( !$ok ) {
		$ok=1;
		print "file $file: " .
		    "(" . $spec{"file"}{$file}{"size"} . " bytes) " .
			localtime($spec{"file"}{$file}{"time"}) .
			    "\n\texists locally, but not on $remhost.\n";
		if ( $deleted ) {
		    print "\t*** has been deleted ***\n";
		    $copyFlag && $delSpecs  && ($mirror ne "local") &&
			( $ans = "del" );
		}
		if ( !$copyFlag ) {
		    $ans = readWithQuit("c:del:ign:IGN:R:s",'s',
					"(c)   copy the file",
					"(del) delete the file",
					"(ign) ignore permanently: add to " .
					"excludeList in $cmd_file",
					"(IGN) ignore permanently: add to " .
					"$skipFilename",
					"(R)   rename",
					"(s)   skip the file");
		}
		if    ($ans =~ /^c/i)   { clientCopyTo($file,$file); }
		elsif ($ans =~ /^del/i) { 
		    sendCmd("delete",$file);
		    Delete($file);
		} elsif ($ans =~ /^s/) {
		} elsif ($ans =~ /^ign/) {
		    ignoreSpec($file,"file",\%spec,\%Rspec,"rc");
		} elsif ($ans =~ /^IGN/) {
		    ignoreSpec($file,"file",\%spec,\%Rspec,"skip");
		} elsif ($ans =~ /^R/) {
		    my @words = split(" ",$ans);
		    my $newName;
		    if ( @words>1 ) {
			$newName = $words[1];
		    } else {
			print "new name: ";
			$newName = readFlushedStdin();
		    }
		    
		    $newName eq '' || fsyncRename($file,$newName);
		    $ans='';
		} 
		else  { 
		    print "invalid response.\n"; 
		    $ans='';$ok=0;
		}
	    }
	}
	delete $spec{"file"}{$file};
    }
    #
    # compare hard links
    #
    $verbose && print STDERR "Processing hard links...\n";
    # check for existance

    foreach my $link ( sort keys %{$Rspec{"hardlink"}} ) {
	$verbose>3 && print STDERR "remote hard link: " .
	    $Rspec{"hardlink"}{$link} . "\n";
	if (!exists $spec{"hardlink"}{$link}) {
	    print "hard link $link --> " .
		$Rspec{"hardlink"}{$link} . 
		    "\n\texists on $remhost, but not locally.\n";
	    my $ans='c';
	    if ( !$copyFlag ) {
		$ans = readWithQuit("c:del:ign:IGN:s",'s',
				    "(c)   create the link locally",
				    "(del) delete the link on $remhost",
				    "(ign) ignore permanently: add to " .
				    "excludeList in $cmd_file",
				    "(IGN) ignore permanently: add to " .
				    "$skipFilename",
				    "(s)   skip");
	    }
	    if    ($ans =~ /^c/i)   { 
		$verbose && print STDERR "creating $link locally...\n";
		link($Rspec{"hardlink"}{$link} , $link); }
	    elsif ($ans =~ /^del/i) { 
		sendCmd("delete",$link);
		Delete($link);
	    } elsif ($ans =~ /^ign/) {
		ignoreSpec($link,"hardlink",\%spec,\%Rspec,"rc");
	    } elsif ($ans =~ /^IGN/) {
		ignoreSpec($link,"hardlink",\%spec,\%Rspec,"skip");
	    } 
	} else {
	    # check that the links match
	    if ($spec{"hardlink"}{$link} ne $Rspec{"hardlink"}{$link} ) {
		print "hard link $link differs.\n";
		print "     local : ".$spec{"hardlink"}{$link}."\n";
		print "     remote: ".$Rspec{"hardlink"}{$link}."\n";
		my $ans='';
		$mirror    && ( $ans = $mirror );
		if ($ans eq '') {
		    $ans = readWithQuit("l:r:del:ign:IGN:s",'s',
					"(l)   copy local to remote",
					"(r)   copy remote to local",
					"(del) delete both links",
					"(ign) ignore permanently: add to " .
					"excludeList in $cmd_file",
					"(IGN) ignore permanently: add to " .
					"$skipFilename",
					"(s)   skip this link");
		}
		if ($ans =~ /^l/i) {sendCmd("hardlink",
					      $spec{"hardlink"}{$link},
					      $link); }
		elsif ($ans =~ /^r/i) {Delete($link);
				       link($Rspec{"hardlink"}{$link},$link);}
		elsif ($ans =~ /^del/i) { Delete($link);
					  sendCmd("delete",$link); }
		elsif ($ans =~ /^ign/) {
		    ignoreSpec($link,"hardlink",\%spec,\%Rspec,"rc");
		} elsif ($ans =~ /^IGN/) {
		    ignoreSpec($link,"hardlink",\%spec,\%Rspec,"skip");
		} 
	    }
	}
    }
    foreach my $link (sort keys( %{$spec{"hardlink"}} )) {
	if (!exists $Rspec{"hardlink"}{$link}) {
	    print "hard link $link --> " .
		$spec{"hardlink"}{$link} . 
		    "\n\texists on locally, but not on $remhost.\n";
#	    print "link $link exists locally, but not on $remhost.\n";
	    my $ans='c';
	    $copyFlag ||
		( $ans = readWithQuit("c:del:ign:IGN:s",'s',
				      "(c)   create the link",
				      "(del) delete the link",
				      "(ign) ignore permanently: add to " .
				      "excludeList in $cmd_file",
				      "(IGN) ignore permanently: add to " .
				      "$skipFilename",
				      "(s)   skip") );
	    if ($ans =~ /^c/i)   { 
		$verbose && print STDERR "creating $link on $remhost...\n";
		sendCmd("hardlink",$spec{"hardlink"}{$link},$link); }
	    elsif ($ans =~ /^del/i) { 
		sendCmd("delete",$link);
		Delete($link);
	    } elsif ($ans =~ /^ign/) {
		ignoreSpec($link,"hardlink",\%spec,\%Rspec,"rc");
	    } elsif ($ans =~ /^IGN/) {
		ignoreSpec($link,"hardlink",\%spec,\%Rspec,"skip");
	    } 
	}
    }
    
	 
    ClientQuit();
        
} # Client 

my $pipe_in = 0;
my $pipe_out = 0;

sub spawnServer {
    my ($rsh,$proxyequalsremote,$remuser,$passcode) = @_;

	
    my $Sverbose="";
    if ($verbose) {
	$Sverbose = "-v" . $verbose;
    }
    if ($port) {
	# make sure port on server side is not listening.
	$port = findUnusedPort($remhost,$port);
	# don't choose proxy port if we use no proxy at all
	if ( $proxyequalsremote ) {
	    $proxyport = $port;
	} else {
	    $proxyport = findUnusedPort($proxyhost,$proxyport);
	}
    }
    #
    #  rsh
    #
    if (! $rsh) {
	$rsh = "rsh " . ( $remuser ? "-l $remuser " : "" ) . "REMHOST";
    }
    $rsh =~ s/REMHOST/$remhost/g;
    $rsh =~ s/PROXYHOST/$proxyhost/g;
    $rsh =~ s/PROXYPORT/$proxyport/g;
    $rsh =~ s/PORT/$port/g;
    
    $verbose>1 && print STDERR "port $port, proxyport $proxyport\n";

    my $cmd = "$progname -s -p $port ";
    $port && ( $cmd .= "$Sverbose $passcode" );

    if( $port) {
	#
	# fork off rsh command to start the fsync server on the remote host
	#
        $verbose && print STDERR "remote command: $rsh $remhost \"$cmd\" > $Slogfile 2>&1 </dev/null\n";
        $verbose && print STDERR "starting fsync server on $remhost " .
	    "using TCP/IP...";
	close(STDIN);
	unless ($pid = fork) {
	    unless (fork) {
		$SIG{'INT'} = 'IGNORE';
		exec "$rsh \"$cmd\" > $Slogfile 2>&1 </dev/null";
		die "no exec";
		exit 0;
	    }
	    exit 0;
	}
	$verbose && print STDERR "\n";
    } else {
	eval { use IPC::Open3; };
        $verbose && print STDERR "remote command: $rsh \'$cmd\'\n";
        $verbose && print STDERR "log file: $Slogfile\n";
        $verbose && print STDERR "starting fsync server on $remhost using " .
	    "stdin/out communication...";

	open(SERVERLOG,">$Slogfile") || warn("error opening $Slogfile");
	$pid = open3(*PIPEOUT,*PIPEIN,">&SERVERLOG",
		     "$rsh \'$cmd\'");
	$pipe_in = *PIPEIN;
	$pipe_out = *PIPEOUT;
	$verbose && print STDERR "\n";
    }
} # spawnServer

sub contactServer {
    my ( $outRef, $port, $proxyhost, $proxyport, $retries ) = @_;

    #
    # initialize connection
    #
    my $in=0;
    my $out=0;
    # special case, for manually started or suspended server.
    my $runningServer=0;
    if ($retries==0) {$runningServer = 1; $retries=1;}
    if ( $port ) {
	use IO::Socket;

	for (my $i=0 ; $i<$retries && (!$in) ; $i++) {
	    $verbose>1 && print STDERR
		"attempting connnection $i to $remhost " .
		    "via port $proxyport at $proxyhost...";
	    my $oldW=$^W;
	    $^W = 0;
	    $in = 
		$nethandle_in = IO::Socket::INET->new(Proto     => "tcp",
						      PeerAddr  => $proxyhost,
						      PeerPort  => $proxyport);
	    $out = $in;
	    $^W=$oldW;
	    $verbose>1 && print STDERR "\n";
	    if ($in) {
		$in->autoflush(1);  # so output gets there right away
		$out->autoflush(1);
		
		#it seems that ssh tunneling can start up before fsync
		# and allow connections to be established. This is to catch and
		# destroy such connections.
		my $response = getLine();
		if ( $response eq "" ) {
		    close($in);
		    close($out);
		    $in = 0;
		    $verbose>1 && 
			print STDERR "\nconnection fault. Retrying...";
		}                  
	    }
	    #wait between retries
	    $runningServer || $in || sleep($retryInterval);
	}
	$runningServer || $in ||
	    print STDERR "$proxyhost:$proxyport NOT CURRENTLY REACHABLE\n";

	# resync stdin: the forked process may have mucked with it.
	$skipFlag ||
	    open(STDIN,"/dev/tty") || die "couldn't open stdin!\n";
    } else { #use stdin/stdout
	$in = $nethandle_in = $pipe_in;
	$out = $pipe_out;
	my $response = getLine();
    }
    $$outRef = $out;
    return $in;
} # contactServer

sub findHashCollisions {
    #given a lhash and a rhash, find specs in field2 which also exist in
    # field1 (and visa-versa)
    # return the (r|l)spec and its type as a hash in (l|r)coll
    my ($lCollR,$rCollR,$lHashR, $rHashR, $field1, $field2) = @_;

    foreach my $lSpec ( keys %{$$lHashR{$field1}} ) {
	if (exists $$rHashR{$field2}{$lSpec}) {
	    $$lCollR{$lSpec} = $field1;
	    $$rCollR{$lSpec} = $field2;
	    $verbose>8&& print STDERR 
		"findHashCollisions: $lSpec: $field1 $field2\n";
	}
	if (exists $$rHashR{"symlink"}{$lSpec}) {
	     $$lCollR{$lSpec} = $field1;
	     $$rCollR{$lSpec} = "symlink";
	     $verbose>8&& print STDERR 
		 "findHashCollisions: $lSpec: $field1 " .
		     "symlink->" . $$rHashR{"symlink"}{$lSpec}{"type"} . "\n";
	 }
     }
    foreach my $lSpec ( keys %{$$lHashR{$field2}} ) {
	if (exists $$rHashR{$field1}{$lSpec}) {
	    $$lCollR{$lSpec} = $field2;
	    $$rCollR{$lSpec} = $field1;
	    $verbose>8&& print STDERR 
		"findHashCollisions: $lSpec: $field2 $field1\n";
	}
	if (exists $$rHashR{"symlink"}{$lSpec}) {
	     $$lCollR{$lSpec} = $field2;
	     $$rCollR{$lSpec} = "symlink";
	     $verbose>8&& print STDERR 
		 "findHashCollisions: $lSpec: $field2 " .
		     "symlink->" . $$rHashR{"symlink"}{$lSpec}{"type"} . "\n";
	 }
     }
    foreach my $rSpec ( keys %{$$rHashR{$field1}} ) {
	if (exists $$lHashR{$field2}{$rSpec}) {
	    $$lCollR{$rSpec} = $field2;
	    $$rCollR{$rSpec} = $field1;
	    $verbose>8&& print STDERR 
		"findHashCollisions: $rSpec: $field2 $field1\n";
	}
	 if (exists $$lHashR{"symlink"}{$rSpec}) {
	     $$lCollR{$rSpec} = "symlink";
	     $$rCollR{$rSpec} = $field1;
	     $verbose>8&& print STDERR 
		 "findHashCollisions: $rSpec: symlink->" .
		     $$lHashR{"symlink"}{$rSpec}{"type"} . " $field1\n";
	 }
    }
    foreach my $rSpec ( keys %{$$rHashR{$field2}} ) {
	if (exists $$lHashR{$field1}{$rSpec}) {
	    $$lCollR{$rSpec} = $field1;
	    $$rCollR{$rSpec} = $field2;
	    $verbose>8&& print STDERR 
		"findHashCollisions: $rSpec: $field1 $field2\n";
	}
	 if (exists $$lHashR{"symlink"}{$rSpec}) {
	     $$lCollR{$rSpec} = "symlink";
	     $$rCollR{$rSpec} = $field2;
	     $verbose>8&& print STDERR 
		 "findHashCollisions: $rSpec: symlink->" .
		     $$lHashR{"symlink"}{$rSpec}{"type"} . " $field2\n";
	 }
    }
} # findHashCollisions

sub deleteHashCollisions {
    #given a local and a remote hash, delete instances in which there is
    # a duplication between first and second entries
    my ($lHashR, $rHashR) = splice(@_,0,2);
    my %lColl;
    my %rColl;
    findHashCollisions(\%lColl,\%rColl,$lHashR,$rHashR,@_);
    foreach my $spec (keys %lColl ){
	print STDERR "the name $spec is a $lColl{$spec} locally\n";
	print STDERR "\tbut a $rColl{$spec} on remHost. Skipping...\n";
	delete $$lHashR{$lColl{$spec}}{$spec};
	delete $$rHashR{$rColl{$spec}}{$spec};
	if ( $lColl{$spec} eq "dir" ) {
	    RemoveDirEntries($lHashR,$spec);
	}
	if ( $rColl{$spec} eq "dir" ) {
	    RemoveDirEntries($rHashR,$spec);
	}
    }
} # deleteHashCollisions

sub printHash {
    my ($hash,$name) = @_;
    print STDERR "$name:\n";
    foreach my $key ( keys(%{$$hash{"$name"}}) ) {
	print STDERR "\t$key\n";
    }
}    

sub savePasscode {
    sendLine("get-passcode");
    my $passcode = getLine();
    open(pcFILE,">$passcodeFilename") || 
	warn("error opening $passcodeFilename");
    print pcFILE "$passcode $remhost $port $proxyhost $proxyport\n";
    close(pcFILE);
    chmod(0600,$passcodeFilename);
} 

sub ClientQuit {

    SaveHashes();
    if ( $stopServer ) {
	sendLine("quit");
    } else {
	savePasscode();
	sendLine("suspend");
    }
    close($nethandle_in);
    close($nethandle_out);
    if ( $pid ) {
	kill($pid);
	waitpid($pid,0); #wait for rsh process (which started the remote sync)
    }
    $port || close(SERVERLOG);
    exit(0);
}

sub clientHandler {
    my $sig = shift;

    print STDERR "Caught signal $sig. Shutting down.\n";
    if ($nethandle_out) {
	sendLine("quit"); #try to shut down server
	close($nethandle_out);
	close($nethandle_in);
    }
    kill($pid);
    waitpid($pid,0); #wait for rsh process (which started the remote sync)
    $port || close(SERVERLOG);
    exit(1);
} # clientHandler

sub warnHandler {
    my $mess = shift;

    $mess =~ /^Use of uninitialized value at/ ||
	print "$mess\n";
}

sub DiffFile {

    my $filename = shift;

    my $remfilename = $filename . ".remote";
    $omitHashEntry=1;
    clientCopyFrom($filename,$remfilename);
    $omitHashEntry=0;

    my $ldiffCmd = $diffCmd;
    my $locProt = quoteShell( $filename );
    my $remProt = quoteShell( $remfilename );

    $ldiffCmd =~ s/Lfile/$locProt/g;
    $ldiffCmd =~ s/Rfile/$remProt/g;

    system($ldiffCmd);

    unlink($remfilename);

}  # DiffFile

sub MergeFile {
    my $filename = shift;

    my $remfilename = $filename . ".remote";
    $omitHashEntry=1;
    clientCopyFrom($filename,$remfilename);

    my $lmergeCmd = $mergeCmd;
    my $locProt = quoteShell( $filename );
    my $remProt = quoteShell( $remfilename );

    $lmergeCmd =~ s/Lfile/$locProt/g;
    $lmergeCmd =~ s/Rfile/$remProt/g;

    system($lmergeCmd);

    clientCopyTo($remfilename,$filename);
    $omitHashEntry=0;
    unlink($remfilename);
    unlink($remfilename . '~');
}  # MergeFile


sub clientCopyFrom {
    # copy file from server to client
    my ($remfilename, $locfilename, $tmpFile) = @_;

    $verbose && 
	print STDERR "Copying $remfilename from $remhost.\n";

    sendLines("copy-from",$remfilename);

    my $filesize = getLine();

    if ($filesize > $promptFilesize) {
	print "$remfilename is $filesize bytes. Really copy?\n";
	my $ans=
	    readInteractive("y:n","n",
			    "the size of $remhost:$remfilename is",
			    "larger than \$promptFilesize ($promptFilesize)",
			    "To avoid this prompt, modify ~/.fsyncrc",
			    "(y)  copy the file",
			    "(n)  skip this file");
	$skipFlag && ( $ans='n' );
	if ( $ans !~ /^y/i ) {
	    sendLine("cancel");
	    return 0;
	}
    }
    sendLine("copy");

    $intCount=0;
    $SIG{'INT'}= eval { sub { $intCount++; 
		     if ( $intCount>2 ) {
			 $setDate && utime 0,0, $locfilename;
			 clientHandler("INT"); 
		     }
		     print STDERR "clientCopyFrom: aborting...\n" }};
    $SIG{'PIPE'} = eval {sub { $setDate && utime 0,0, $locfilename; 
			     clientHandler("INT"); }};
    CopyFrom($locfilename,$tmpFile);
    $SIG{'INT'} = $SIG{'PIPE'} =  'clientHandler';
    checkError();

    return 1;	
} # clientCopyFrom


sub clientCopyTo {
    # copy file from client to server
    my $locfilename = shift;
    my $remfilename = shift;

    $verbose && 
	print STDERR "Copying $locfilename to $remhost.\n";

    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$filesize,$atime,$mtime)
	= stat($locfilename);
    if ($filesize > $promptFilesize) {
	print "$locfilename is $filesize bytes. Really copy?\n";
	my $ans=
	    readInteractive("y:n","n",
			    "the size of $locfilename is",
			    "larger than \$promptFilesize ($promptFilesize)",
			    "To avoid this prompt, modify ~/.fsyncrc",
			    "(y)  copy the file",
			    "(n)  skip this file");
	$skipFlag && ( $ans='n' );
	if ( $ans !~ /^y/i ) {
	    return 0;
	}
    }
    sendLines("copy-to",$remfilename);

    $intCount=0;
    $SIG{'INT'}= sub { $intCount++; 
		     if ( $intCount>2 ) {
			 $setDate && utime 0,0, $locfilename;
			 clientHandler("INT"); 
		     }
		     print STDERR "clientCopyTo: aborting...\n"};
    $SIG{'PIPE'} = eval {sub { $setDate && utime 0,0, $locfilename; 
			     clientHandler("INT"); }};
    CopyTo($locfilename);
    $SIG{'INT'} = $SIG{'PIPE'} =  'clientHandler';
    checkError();
} # clientCopyTo

sub readInteractive {
    my ($options,$default,@helps) = @_;

    print "action (?:$options) [$default] : ";
    if ( $skipFlag ) {
	print "\n";
	return $default;
    }

    while (1) {
	my $ans = readFlushedStdin();
##	print "response: >$ans<\n";
	foreach my $option ( split(/:/, $options) ) {
	    ( $ans =~ /^$option/i ) && return $ans;
	}
	( $ans =~ /^[\n]?$/i ) && return $default;
	if ( $ans =~ /^help/i || $ans =~ /^\?/) {
	    foreach my $help ( @helps ) {
		print "\t$help\n";

	    }
	} else {
	    print "invalid response.\n";
	}
	print "action ($options:?) [$default] : ";
    }
} # readInteractive

sub readWithQuit {
    my ($options,$default,@helps) = @_;
    $options .= ":q:sus";
    @helps = (@helps, 
	      "(q)   quit fsync",
	      "(sus) suspend fsync: leave server running");
    my $ans = readInteractive($options,$default,@helps);

    $ans =~ /^q/i && ClientQuit();
    if ( $ans =~ /^sus/i ) {
	$stopServer = 0;
	ClientQuit();
    }
    return $ans;
}

sub sendCmd {
    sendLines(@_);
#    foreach my $i (@_) { sendLine($i); } 
    checkError();
}


##################################################
#
# server code
#
##################################################

sub Server {
    use Net::hostent;              # for OO version of gethostbyaddr
    use Cwd;

    select(STDERR);
    $|=1;

    my $maxUnknown = 100;
    my $numUnknown = 0;
    
#    $0 .= " -s";

    my $initDir = getcwd();
    chdir();  # change to home dir- for manually started server
    my $homeDir = getcwd();
    #make $0 absolute, if necessary
    if (($homeDir ne $initDir) && ($0 !~ /^\//)) { 
	$0 = $initDir . "/" . $0;
    }

    my $cmdline = join(" ",@_);

    my $root_dir = ".";
    my $port = $portDefault;
    while (scalar(@_)>0 && $_[0] =~ /^-/) {
	if ($_[0] =~ /^-v$/) {
	    shift;
	    $verbose = 1;
	} elsif ($_[0] =~ /^-v([0-9])/) {
	    shift;
	    $verbose = $1;
	} elsif ($_[0] =~ /^-V/) {
	    print "$version\n";
	    exit();
	} elsif ($_[0] eq '-p') {
	    shift;
	    $port = shift;
	} else {
	    Usage();
	    die "$0: invalid option.\n";
	}
    }
    my @fileList;
    my $passcode="";
    @_ && ( $passcode = shift );
    
    #
    # install signal handler
    #
    $SIG{'INT'} = $SIG{'QUIT'} = 'serverHandler';
    $SIG{'HUP'} = 'IGNORE';

    $verbose && print STDERR "running server with port $port\n";
    $verbose && print STDERR "running server as $progname $cmdline\n";

    if ($port) {
#	eval { use IO::Socket; };
    init_server:
      my ($server, $hostinfo);
      
      $server = IO::Socket::INET->new( Proto     => 'tcp',
				       LocalPort => $port,
				       Listen    => 1,
				       Reuse     => 1);
      
      die "can't setup server on port $port: $!. Try a different port." 
	unless $server;
      $verbose && print STDERR "[Server $0 accepting clients]\n";
      
      $server->sockopt(SO_REUSEADDR,1);	#is this needed?
      if ($nethandle_in = $server->accept()) {
	$nethandle_out = $nethandle_in;
	#
	# the close command stops this server from listening on the port
	# this enables another instance of fsync to run simultaneously.
	#
	close($server);
	$nethandle_in->autoflush(1);
	$hostinfo = gethostbyaddr($nethandle_in->peeraddr);
	$verbose && printf STDERR "[Connect from %s]\n", $hostinfo->name || $nethandle_in->peerhost;
      }
    } else {
      $nethandle_in = *STDIN;
      $nethandle_out = *STDOUT;
    }    
    if ($nethandle_in) {
	#print $nethandle_out "Welcome to $0; type help for command list.\n";
	sendLine("passcode:");
	my $cpasscode = getLine();
	my $ok = 1;
	if ( $passcode ne "" &&  $passcode ne $cpasscode ) {
	    print STDERR "invalid passcode.\n";
	    $ok=0;
	}
	my (@ldbCSum, @rdbCSum);
	my $dbFilename;
	while ($ok) {
	    $_ = <$nethandle_in>;
	    $verbose>1 && print STDERR "server: cmd: $_\n";
	    my ($cmd, $arg1, $arg2, $arg3) = split;
	
	    if    ($cmd=~/^backup\-dir/i){ $backupDir = getLine(); }
	    elsif ($cmd=~/^checksum\-prog/i) { $checkSumProg = getLine();
					       initCheckSum(); }
	    elsif ($cmd=~/^copy\-from/i) { my $filename = getLine();
					   ServerCopyTo($filename); }
	    elsif ($cmd=~/^\Qcopy-to\E/i){ my $filename = getLine();
					   ServerCopyFrom($filename); }
	    elsif ($cmd=~/^delete/i)     { my $filename = getLine();
					   Delete($filename); }
	    elsif ($cmd=~/^check-diskspace/i) { 
		if ( checkDiskSpace( $arg1 ) ) {
		    sendLine( "ok" );
		} else {
		    sendLine( "failed" );
		}
	    } elsif ($cmd=~/^file\-list/i) { 
		getArray( \@fileList , $arg1 );
	    } elsif ($cmd=~/^fsuid/i) { $hashSaveFile =~ s/FSUID/$arg1/g;
				      my $fsuid = readFsuid();
				      sendLine( $fsuid );
				  }
	    elsif ($cmd=~/^get\-error/i) { sendLine($errorMsg);
					   $errorMsg = "";
					 }
	    elsif ($cmd=~/^get\-hash/i)  { 
		sendHash2D($spec{"file"});
		sendHash2D($spec{"symlink"});
		NsendHash($spec{"hardlink"});
		NsendHash($spec{"dir"});
		sendLine( scalar(@skippedList) );
		sendArray( \@skippedList );
	    } elsif ($cmd=~/^\Qget-passcode\E/i) {  
		$passcode = int(2**32 * abs(rand()));
		sendLine($passcode);
		$errorMsg = "";
	    }
            elsif ($cmd=~/^hash\-save\-file/i) { $hashSaveFile = getLine(); }
            elsif ($cmd=~/^skip\-file/i) { $skipFilename = getLine(); }
	    elsif ($cmd=~/^\Qin-hash-save\E/i) {
		my $spec = getLine();
		exists $timeHashSave{$spec} && sendLine("1");
		exists $timeHashSave{$spec} || sendLine("0");
	    }
	    elsif ($cmd=~/^\Qmake-hash\E/i) { 
		getArray( \@excludeList, $arg1 );
		MakeHashes(@fileList);
	    } elsif ($cmd=~/^\Qdb-send-csum\E/i) { 
		getArray( \@rdbCSum, $arg1 );
	    }
	    elsif ($cmd=~/^\Qdb-get-csum\E/i) { 
		sendLine( scalar(@ldbCSum) );
		sendArray( \@ldbCSum );
	    } elsif ($cmd=~/^\Qdb-make-csum\E/i) { $dbFilename = getLine();
						 NgetRaw( \$dbDelim );
						 dbCheckSum($dbFilename,
							    \@ldbCSum);
					     }
	    elsif ($cmd=~/^\Qdb-copy-from\E/i) { dbCopyTo( $dbFilename ); }
	    elsif ($cmd=~/^\Qdb-copy-to\E/i) { 
		$SIG{'PIPE'} = eval {sub { $setDate && utime 0,0, $dbFilename; 
					 serverHandler("INT"); }};
		dbCopyFrom( $dbFilename , \@rdbCSum , \@ldbCSum );
		$SIG{'PIPE'} = 'serverHandler';
	    }
	    elsif ($cmd=~/^mkdir/i) { 
		my $dirname = getLine();
		makedir($dirname,$arg1,$arg2,$arg3);
	    }
	    elsif ($cmd=~/^rename/i) { 
		my $from = getLine();
		my $to = getLine();
		fsyncRename($from,$to);
	    }
	    elsif ($cmd=~/^quit/i)       { $ok = 0; SaveHashes();}
	    elsif ($cmd=~/^rootdir/i)    { my $dirname = getLine();
					   chdir($dirname) || 
					       setError("chdir $dirname " .
							"failed.");}
	    elsif ($cmd=~/^\Qset-date\E/i ) { 
		$setDate  = $setDate?0:1;
		$verbose && print "setDate: $setDate\n"; }
	    elsif ($cmd=~/^\Qset-own\E/i) { 
		$setOwn = $setOwn?0:1;
		$verbose && print "setOwn: $setOwn\n"; }
	    elsif ($cmd=~/^\Qget-own\E/i) { 
		my $spec = getLine();
		my ($dev,$ino,$mode,$nlink,$uid,$gid) = stat($spec);
		sendLine($uid);
		sendLine($gid);
	    }
	    elsif ($cmd=~/^\Qset-perms\E/i) { 
		$setPerms = $setPerms?0:1;
		$verbose && print "setPerms: $setPerms\n"; }
	    elsif ($cmd =~/^suspend/i) {
		chdir($homeDir);
		SaveHashes();
		print STDERR "server: restarting\n";
		$numUnknown = 0;
		%spec = ();
		%timeHashSave = ();
		%sizeHashSave = ();
		%csumHashSave = ();
		@crctab = ();
		@skippedList = ();
		@fileList = ();
		$errorMsg = "";
		$hashMiss = 0;
		goto init_server;
	    }
	    elsif ($cmd=~/^\Qsync-hash\E/i) { 
		my $file = getLine();
		syncHash($file);
	    }
	    elsif ($cmd=~/^symlink/i)    { my $source = getLine();
					   my $link   = getLine();
					   Delete($link);
					   makeSymlink($source,$link);}
	    elsif ($cmd=~/^hardlink/i)   { my $source = getLine();
					   my $link   = getLine();
					   Delete($link);
					   link($source,$link) ||
					       setError("link $source $link " .
							"failed."); }
	    elsif ($cmd=~/^verbose/i)    { $verbose = $arg1; }
	    elsif ($cmd=~/^\Qfollow-symlinks\E/i) { $followSymlinks = $arg1; }
	    elsif ($cmd=~/^\Qfollow-toplinks\E/i) { $followTopLinks = $arg1; }
	    elsif ($cmd=~/^version/i)    { sendLine($versionNum); }
	    else {
		setError("unknown command: $cmd");
		if ( $numUnknown++ > $maxUnknown ) {
		    print STDERR "too many unknown commands: $numUnknown\n";
		    $ok=0;
		}
	    }
	}
    }
    close $nethandle_in;
    close $nethandle_out;

}

sub makeSymlink {
    my ($target,$link) = @_;
    my $ret = symlink($target,$link);

    #for dangling link symlink retval is incorrect
    $ret || (Exists($link)  && (readlink($link) eq $target)) && ( $ret = 1 );
    
    if ( $ret == 1 ) {
	syncHash($link);
    } else {
	my $msg = "makeSymlink: error making link $link --> $target";
	print STDERR "$msg\n";
	$errorMsg = $msg;
    }
}

sub serverHandler {
    my $sig = shift;

    print STDERR "Caught signal $sig. Shutting down.\n";
    close($nethandle_in);
    close($nethandle_out);
    exit(1);
}

sub ServerCopyTo {
    my $filename = shift;

    my ($dev,$ino,$filemode,$nlink,$uid,$gid,$rdev,$filesize,
	$atime,$filetime,$ctime,$blksize,$blocks)
	= stat($filename);
    sendLine($filesize);

    # see if we should continue
    $_ = getLine();
    if ( /cancel/ ) {
	return;
    }

    CopyTo($filename);

}

sub ServerCopyFrom {
    my $filename = shift;

    $SIG{'PIPE'} = eval {sub { $setDate && utime 0,0, $filename; 
			     clientHandler("INT"); }};
    CopyFrom($filename);
    $SIG{'PIPE'} =  'serverHandler';

}

##################################################
#
# subroutines used by both fsync and fsyncd
#
##################################################

sub readFsuid {
    my $fsuid = "";
    if (-f $fsuidFilename) {
	open(fsuidFILE,"$fsuidFilename") || 
	    die("error opening $fsuidFilename");
	$fsuid = <fsuidFILE>;
	chomp( $fsuid );
	close(fsuidFILE);
    	$verbose>1 && 
	    print STDERR "fsuid: $fsuid\n";
    } else { # no fsuid: generate one
	use Sys::Hostname;
	$fsuid = hostname() . "-" . int(1024*abs( rand() ));
	open(fsuidFILE,">$fsuidFilename") || 
	    die("error opening $fsuidFilename for write");
	print fsuidFILE "$fsuid\n";
	close(fsuidFILE);
    }
    return $fsuid;
} # readFsuid

sub syncHash {
    my $specname = shift;
    $omitHashEntry && return;
    if ( exists($spec{"file"}{$specname}) ) {
	$sizeHashSave{$specname} = $spec{"file"}{$specname}{"size"};
	$timeHashSave{$specname} = $spec{"file"}{$specname}{"time"};
	$csumHashSave{$specname} = $spec{"file"}{$specname}{"csum"};
    } elsif ( exists($spec{"symlink"}{$specname})  ) {
	my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime)
	    = lstat($specname);
	my $target = readlink($specname);
	$sizeHashSave{$specname} = $spec{"symlink"}{$specname}{"size"};
	$timeHashSave{$specname} = $mtime;
	$csumHashSave{$specname} = checkSumString($target);
    } else {
	$sizeHashSave{$specname} = 0;
	$csumHashSave{$specname} = 0;
	$timeHashSave{$specname} = 0;
    }
} # syncHash


sub readFlushedStdin {
    #
    # clear any garbage the user may have typed before this call- with this,
    # the user can't type ahead of the program.
    #
    my ($rin, $win, $ein) = ('', '', '');
    vec($rin,fileno(STDIN),1)=1;
    my $b;
    while (select($rin, $win, $ein,0)) {sysread(STDIN,$b,1);}
    $b = <STDIN>;
    return $b;
}

sub sendLine {
    my $mess = shift;
    print $nethandle_out "$mess\n";
    $nethandle_out->flush();
    $verbose>8&&print STDERR "sendLine: $mess\n";
}

sub sendLines { foreach my $i (@_) { sendLine($i); } }

sub getLine {
    $_ = <$nethandle_in>;
    defined($_) || ($_="\n");
    chop;
    $verbose>8&&print STDERR "getLine: $_\n";
    return $_;
}
    
sub NsendRaw {
    my $bufref = shift;

    sendLine(length($$bufref));
    print $nethandle_out $$bufref;
    $nethandle_out->flush();
}

sub NgetRaw {
    my $bufref = shift;

    my $size = getLine();
    $$bufref = pack("x$size"); #preallocate buffer    
    my $rsize = read($nethandle_in,$$bufref,$size);

    ($rsize != $size) && 
	fsDie("NgetRaw: read $rsize bytes instead of $size bytes.\n");
    return $size;
}

sub sendArray {
    my $arrRef = shift;

    for (my $i=0 ; $i<scalar(@$arrRef) ; $i++) {
	NsendRaw(\$$arrRef[$i]);
    }
} # sendArray

sub getArray {
    my ($arrRef,$len) = @_;
    
    for (my $i=0 ; $i<$len ; $i++) {
	my $tmp;
	NgetRaw(\$tmp);
	push( @$arrRef , $tmp);
    }
}

sub NsendHash {
    my $hashref = shift;

    sendLine(scalar(keys(%$hashref)));
    my $key;
    foreach $key (keys(%$hashref)) {
	$verbose>8 && 
	    print STDERR "$key: " . $$hashref{$key} . "\n";
	NsendRaw(\$key);
	NsendRaw(\$$hashref{$key});
    }
}

sub sendHash2D {
    my $hashr = shift;
    
    my @specs = keys(%$hashr);
    sendLine(scalar(@specs));
    my @fields = keys( %{$$hashr{$specs[0]}} );
    sendLine(scalar(@fields));
    foreach my $field ( @fields ) {
	NsendRaw(\$field);
    }
    foreach my $spec ( @specs ) {
	NsendRaw(\$spec);
	foreach my $field ( keys( %{$$hashr{$spec}} ) ) {
	    $verbose>8 && 
		print STDERR "$spec: $field: " . 
		    $$hashr{$spec}{$field} . "\n";
	    NsendRaw(\$$hashr{$spec}{$field});
	}
    }
} #sendHash2D
    

sub getHash2D {
    my $hashr = shift;
    
    %$hashr = ();   #initialize to nothing

    my $numSpecs = getLine();
    my $numFields = getLine();
    my @fields = ();
    for (my $i=0 ; $i<$numFields ; $i++) {
	my $field;
	NgetRaw(\$field);
	push(@fields,$field);
    }
    for (my $i=0 ; $i<$numSpecs ; $i++) {
	my $spec;
	NgetRaw(\$spec);
	foreach my $field ( @fields ) {
	    NgetRaw(\$$hashr{$spec}{$field});
	    $verbose>8 && 
		print STDERR "getHash2D: $spec: $field " . 
		    $$hashr{$spec}{$field} . "\n";
	}
    }
} # getHash2D


sub NgetHash {
    my $hashref = shift;
    
    %$hashref = ();   #initialize to nothing

    my $numkeys = getLine();
    for (my $i=0 ; $i<$numkeys ; $i++) {
	my $key;
	NgetRaw(\$key);
	NgetRaw(\$$hashref{$key});
	$verbose>8 && print "$key: $$hashref{$key}\n";
    }
} # NgetHash

sub makedir {
    #
    # make a directory with given name, time and permissions
    #
    my ($name, $mode, $uid, $gid) = @_;

    mkdir($name,0777);
    $setPerms && chmod($mode,$name);
    $setOwn && chown($uid,$gid,$name);
    syncHash( $name );
} # makedir
    

sub CopyTo {
    #
    # copy local file to remote, given the arguments below
    #
    my $filename = shift; #local filename

    my ($dev,$ino,$filemode,$nlink,$uid,$gid,$rdev,$filesize,
	$atime,$filetime,$ctime,$blksize,$blocks)
	= stat($filename);

    #
    # read local file off the disk
    #
    chmod(0666,$filename);      #allow reads
    $filename =~ s#^(\s)#./$1#;
    if ( !open(LFILE,"< $filename\0") ) {
	setError("CopyTo: error opening $filename.\n");
	$intCount=1; #abort copy
    }

    sendLine($filesize);
    my $filebuf;
    my $size=$filesize;
    do {
	#
	# send file to remote host
	#
	if ( getLine() == 1 ) {
	    $size -= sysread(LFILE,$filebuf,$copyBufferSize);
	    if ($intCount) { $filebuf=""; }
	    NsendRaw( \$filebuf );
	} else {
	    $filebuf="";
	}
#	print "$filename: $size bytes remaining.\n";
    } while ($filebuf);

    close LFILE;

    chmod($filemode,$filename); #restore permissions

    #send file time;
    $intCount && ( $filetime=0 );
    sendLine($filetime);
    
    # send permissions
    sendLine($filemode);
    $setOwn && ( sendLine($uid) , sendLine($gid) );

    syncHash($filename);
} # CopyTo

sub Exists {
    # because -e follows symlinks
    my $spec = shift;
    return ( -e $spec || -l $spec );
}

sub makeBackup {
    #
    # make a backup copy of the given filename, if it exists. The backup
    # copy will have a unique name, such that all old versions of a particular
    # file will be retained. Note that this subroutine renames the file-
    # it no longer exists under the name filename
    #
    my $filename = shift;
    
    my $lfileRef=0;
    -f $filename && chmod(0600,$filename);      #allow reads/writes
    -d $filename && chmod(0700,$filename);
    if ( -f $filename &&
	 !openLocked(\$lfileRef,$filename,"<") ) {
	setError("makeBackup: " .
		 "FATAL ERROR: unable to obtain lock on  $filename");
	SaveHashes();
	die "locking failed.";
    }
    -f $filename && closeLocked(\$lfileRef);

    $backupDir eq "none" && return;
    $backupDir !~ /^\// && ( $backupDir = getcwd() . '/' . $backupDir);

    
    my @dirlist = split(/\//,$filename);
    my $file = pop(@dirlist);

    #
    # create the destination director hierarchy
    #
    my $destDir = "";
    foreach my $name ($backupDir, @dirlist) {
	$destDir .= $name . '/';
	#print STDERR "checking $destDir\n";
	if (! -d $destDir) {
	    mkdir($destDir,0700) ||
		print STDERR "makeBackup: error creating $destDir.\n";
	}
    }
    my $newName = $destDir . $file;

    #
    # create unique name by appending ".m", where m is the smallest
    # natural number which accomplishes the task.
    #
    my $i=0;
    my $uName;
    do {
	$uName = $newName . ".$i";
	$i++;
    } while ( Exists($uName) );

    fsyncRename( $filename, $uName );
    
} # makeBackup

sub fsyncRename {
    my ( $from, $to ) = @_;
    if ( $to !~ /^\// ) {
	# if not absolute, make $to relative to directory of $from
	my $pref = $from;
	$pref =~ s/\/[^\/]+$//;  #FIX: OS dependency
	$pref =~ s/\/[^\/]+$\///; # case in which $from comes w/ trailing /
	$to = $pref . '/' . $to;
    }
    $verbose>8 && 
	print STDERR "fsyncRename: moving\n\t $from --> $to\n";
    $to =~ s/\/+$//; # OS dependent stuff
    my $tree = $to;
    $tree =~ s/\/[^\/]+$//;
    my @dirElms = split(/\//,$tree);
    shift @dirElms; # get rid of leading null field
    $tree = "";
    foreach my $el ( @dirElms ) {
	$tree .= '/' . $el;
	#print STDERR "directory checking $tree\n";
	-d $tree || mkdir($tree,0777);
    }
	
	
    #1) check that to is valid: don't allow relative symlinks change dir
    #2) check that $to doesn't exist (could prompt for overwrite)
    if ( Exists( $to ) ) {
	setError("clientRename: $to already exists");
	return;
    }
    #3) perform move
    if ( -d $from) {
	if ( !move($from, $to) ) {
	    print STDERR "fsyncRename: error creating $to: $!\n";
	    print STDERR "\tattempting tar copy...\n";
  	    $from = File::Spec->catfile($rootdir,$from);
  	    #$from = $rootdir . '/' . $from;
	    mvTree($from,$to);
	}
    } elsif (-l $from) {
	rename($from, $to) || 
	    setError("fsyncRename: error creating $to: $!");
    } else {
	move($from, $to) || 
	    setError("fsyncRename: error creating $to: $!");
    }
    #4) check if new name is in remSpec
} # fsyncRename

sub mvTree {
    mvTreeFind(@_);
}

my $mvTree_from    = "";
my $mvTree_to      = "";
my @mvTree_dirList = ();

sub mvTree_wanted {
	# translate from-dirname to to-dirname 
	$verbose>3 && print STDERR "mvTree_wanted: processing $_\n";
	my $old_ = $_;

	my $fromDir = $File::Find::dir;
	#my $fromDir = File::Spec->canonpath($File::Find::dir);
	$fromDir =~ s!//!/!g;
	grep(/^\Q$fromDir\E$/,@mvTree_dirList) || 
	    push(@mvTree_dirList,$fromDir);
	my $toDir = $fromDir;
	$toDir =~ s/^\Q$mvTree_from\E/$mvTree_to/;
	$verbose>3 && print STDERR "mvTree_wanted: dir $fromDir -> $toDir\n";
	# create dir if it doesn't exist
	
	Exists($toDir) || File::Path::mkpath(($toDir,),0,0700) || 
	    print STDERR "mvTree_wanted: failed to create $toDir\n";
	# translate from-filename to to-filename
	my $fromFile= $File::Find::name;
	-d $fromFile && (grep(/^\Q$fromFile\E$/,@mvTree_dirList) ||
	    push(@mvTree_dirList,$fromFile));

	my $toFile = $fromFile;
	$toFile =~ s/^\Q$mvTree_from\E/$mvTree_to/;
	$verbose>1 && print "mvTree_wanted: renaming $fromFile to $toFile\n";
	-d $fromFile || move($fromFile,$toFile) ||
	    print STDERR "mvTree_wanted: error moving $fromFile\n";
	# rename the file
	$_ = $old_;
} # mvTree_wanted

sub mvTreeFind {

    my ($from , $to) = @_;

    $mvTree_from = File::Spec->canonpath($from);
    $mvTree_from =~ s!//!/!g; #delete?
    $mvTree_to = File::Spec->catfile($rootdir,$to);
    #$mvTree_to = $rootdir . '/' . $to;
    $mvTree_to = File::Spec->canonpath($mvTree_to);
    $mvTree_to =~ s!//!/!g;   #delete?

    @mvTree_dirList = ($mvTree_from);

    $verbose>5 && 
	print STDERR "mvTreeFind: running find(mvTree_wanted,$mvTree_from)\n";
    find(\&mvTree_wanted,$mvTree_from);
    @mvTree_dirList = reverse( @mvTree_dirList );
    foreach my $spec (@mvTree_dirList) { 
	rmdir "$spec" || print STDERR "mvTreeFind: error removing $spec\n";
    }
} # mvTreeFind


sub mvTreeTar {
    #ugly hack to copy directory trees across filesystems
    my ($src,$dest) = @_;
    
    mkdir($dest,0700) ||
	print STDERR "mvTree: error creating $dest: $!\n";
    if ( system("(cd $src; tar cf - .)|(cd $dest; tar xf -)")==0 ) {
	system("rm -rf $src");
    }
} # mvTreeTar

sub openLocked {
    my ($fileref,$filename,$mode) = @_;

    $filename =~ s#^(\s)#./$1#; #protect whitespace
    my $ret;
    ( $ret = open(LOCKEDFILE,$mode . " $filename\0") ) ||
	setError("openLocked: error opening $filename\n");
    $ret || return $ret;
    $$fileref = *LOCKEDFILE;
    $verbose>1 && print STDERR "openLocked: locking $filename\n";
    for (my $i=0 ; $i<10 ; $i++) {
	$ret = flock(LOCKEDFILE,LOCK_EX|LOCK_NB);
	$ret && last;
	print STDERR "openLocked: waiting for lock on file $filename...\n";
	sleep 10;
    }
    $ret || setError("openLocked: " .
		     "could not obtain lock for file $filename...\n");
    return $ret;
}

sub closeLocked {
    my $fileref = shift;
    if ( defined( $fileref ) ) {
	flock($fileref,LOCK_UN);
	close($fileref);
    }
}
    
sub CopyFrom {
    #
    # copy file from remote
    #
    my ($filename) = @_; #local filename, and tmp file flag

    #
    # make a backup copy of the local file
    #
    Exists($filename) && makeBackup($filename);

    #
    # write file locally 
    #
    chmod(0666,$filename);      #allow writes

    my $lfileRef;
    if ( !openLocked(\$lfileRef,$filename,">") ) {
	setError("CopyFrom: error opening file $filename");
	$intCount=1; #abort copy
    }

    my $filebuf;
    my $filesize = getLine(); #not needed
    my $size = 0;
    do {
	#
	# read file from remote host
	#
	if ( $intCount ) {
	    sendLine("0");
	    $filebuf="";
	} else {
	    sendLine("1");
	    $size += NgetRaw(\$filebuf);
	    syswrite($lfileRef,$filebuf,length($filebuf));
	}	    
    } while ( $filebuf );
    
    closeLocked($lfileRef);

    #
    # set date and mode
    #
    my $filetime = getLine();
    $intCount && ( $filetime=0 );
    $setDate && utime $filetime, $filetime, $filename;
    

    my  $filemode = getLine();
    $setPerms || ($filemode=0666 &~ umask()); #mode set in CheckSum

    if ( $setOwn ) {
	my  $fileuid = getLine();
	my  $filegid = getLine();
	chown($fileuid,$filegid,$filename);
    }

    #
    # update the local hashSave info .... need to think about this...
    #
    #  --don't really want to save temporary (used for merging) files
    # add: could check if the hash already exists, and then add

    if ( !$omitHashEntry ) {
	$timeHashSave{$filename} = $filetime;
	$sizeHashSave{$filename} = $size;
	$csumHashSave{$filename} = CheckSum($filename,$filemode);
    }

    return 1;	
} # copyFrom

sub Delete {
    my $filename = shift;

    exists $timeHashSave{$filename} && delete $timeHashSave{$filename};
    if ( Exists($filename) ) {
	$verbose>1 && print STDERR "Delete: removing $filename to fsync-save area\n";
	makeBackup($filename);
	# remove hashsave entry
    } 
}
    

sub MakeHashes {
    #
    # make calls to make hashes given a list of filename
    #
    my @fileList = @_;

    if ($hashSaveFile ne "none") {
	# HOME had better start with a slash!
	$hashSaveFile =~ m!^/! || 
	    ( $hashSaveFile = $ENV{"HOME"} . '/' . $hashSaveFile );
	#
	#  read a list of saved file info
	#
	if (scalar(open(HASHSAVE,$hashSaveFile))) {
	    while (<HASHSAVE>) {
		if ( /^(.*) ([0-9]+) ([0-9]+) ([0-9]+)$/ ) {
		    my $filename = $1;
		    $timeHashSave{$filename} = $2;
		    $sizeHashSave{$filename} = $3;
		    $csumHashSave{$filename} = $4;
		    $verbose>8 &&
			print STDERR "read hashSave info: $1 $2 $3 $4\n";
		} else {
		    print "found munged line in $hashSaveFile: \n$_\n";
		}
	    }
	    close(HASHSAVE);
	} else {
	    $verbose && 
		print STDERR "Could not open hash file $hashSaveFile\n";
	}
    }
	
    foreach my $filename (@fileList) {
	$verbose > 1 && print STDERR "MakeHashes: processing $filename\n";
	my $subdirname="";
	my $recurseFlag = 1;
	$followTopLinks && ( $followSymlinks = 1 );
	do {
	    #
	    # extract all directories higher in the hierarchy (relative to
	    # root) and generate hashes for these, too
	    #
	    MakeHash($filename,$recurseFlag);
	    $recurseFlag = 0;   #don't do recursive MakeHash for higher
	                        # ddirectory entries
	    $filename =~ s/(\/[^\/]*)$//; 
	    if ( defined($1) ) {$subdirname = $1 } else {$subdirname="";}
        } until ( $subdirname eq "" );
    }
    #
    # find hard-linked files
    #
    my @files = sort keys %{$spec{"file"}};
    for (my $i=0 ; $i<@files ; $i++) {
	my $filei = $files[$i];
	if ( exists $spec{"file"}{$filei}{"inode"} ) {
	    my $foundNode=0;
	    for (my $j=$i+1 ; $j<@files ; $j++) {
		my $filej = $files[$j];
		if ( exists $spec{"file"}{$filej}{"inode"} &&
		     $spec{"file"}{$filei}{"inode"} == 
		     $spec{"file"}{$filej}{"inode"} ) {
		    $foundNode=1;
		    ($verbose>2) &&
			print STDERR
			    "found hard link between $filei and $filej\n";
		    $spec{"hardlink"}{$filej} = $filei;
		    delete $spec{"file"}{$filej};
		    splice(@files,$j,1); $j--;
		}
	    }
	    delete $spec{"file"}{$filei}{"inode"};
	    $foundNode || $verbose>0 &&
		print STDERR "did not find hard link for file $files[$i]\n";
	}
    }
    if ($verbose>3) {
	printHash(\%spec,"file");
	printHash(\%spec,"symlink");
	printHash(\%spec,"hardlink");
	printHash(\%spec,"dir");
    }
}

sub SaveHashes {
    #
    # save hash info - the hashes need to be updated whenever a file is copied
    #                  from the other host, or if a merge is done.
    #
    $hashSaveFile eq "none" && return;

    my $fileRef;
    if ( openLocked(\$fileRef,$hashSaveFile,">") ) {
	foreach my $file (sort keys(%timeHashSave)) {
	    defined $csumHashSave{$file} || next;
	    my $field = "$file $timeHashSave{$file} $sizeHashSave{$file} " .
		"$csumHashSave{$file}";
	    $verbose>8 && print STDERR "saveHashes: writing: $field\n";
	    print $fileRef "$field\n";
	}
	closeLocked($fileRef);
    } else {
	print STDERR 
	    "SaveHashes: error opening hashSaveFile: $hashSaveFile.\n";
    }

    $verbose && print STDERR "number of hash misses: $hashMiss\n";
} # end of SaveHashes

sub MakeHash {
    #
    # given the name of a file, symlink or directory, recursively determine
    # checksums
    #
    my $filename    = shift;
    my $recurseFlag = shift;

    $verbose>8&&print STDERR "MakeHash: processing $filename : ";
#    -e $filename && print "$filename exists\n";
	if ( (! $followSymlinks) && (-l $filename) ) {
	    $verbose>8 && print STDERR "symlink -> ";
	    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime)
		= lstat(_);
	    my $target = readlink($filename);
	    $verbose>8 && print STDERR "$target\n";
	    $spec{"symlink"}{$filename}{"target"} = $target;
	    #$spec{"symlink"}{$filename}{"time"}   = $mtime;
	    readFromHashSave( \%{$spec{"symlink"}},$filename, 
			      0,$mtime,$target,$mode);
	    stat( $filename );
	    $spec{"symlink"}{$filename}{"type"}   = "none";
	    -f _ && ( $spec{"symlink"}{$filename}{"type"}   = "file" );
	    -d _ && ( $spec{"symlink"}{$filename}{"type"}   = "dir" );
	} elsif (-d $filename) {
	    $verbose>8 && print STDERR "directory\n";
	    $verbose>8 && print STDERR "recurse: $recurseFlag\n";
	    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime);
	    ($dev,$ino,$spec{"dir"}{$filename})
		= stat(_);
#	    readFromHashSave( \%{$spec{"dir"}},$filename, 
#			      0,0,"dir",$spec{"dir"}{$filename});
	    $followTopLinks && ( $followSymlinks = 0 );
	    if ($recurseFlag) {
		my $skipFile = $filename . '/' . $skipFilename;
		my @lExcludeList = ();
		if ( open(SKIPFILE,$skipFile) ) {
		    $verbose>8&&print STDERR "contents of $skipFile:\n";
		    while (<SKIPFILE>) {
			chop;
			length() || next; #skip blank entries
			my $re = $filename . '/' . $_;
			$verbose>8&&print STDERR "\t$re\n";
			push(@lExcludeList,$re);
		    }
		}
		opendir(DIR1,$filename) || warn("error opening $filename");
		DIRLOOP: 
		foreach my $file (readdir(DIR1)) {
		    $file = $filename . '/' . $file; #get full relative path
		    $verbose>8 && print STDERR "consider $file\n";
		    my $excluded=0;
		    foreach my $re (@excludeList) {
			if ($file =~ /$re/) {
			    $excluded=1;
			    $verbose>5&& 
				print STDERR "\t excluded due to match with "
				    . $re . "\n";
			    next;
			}
		    }
		    foreach my $re (@lExcludeList) {
			if ($file =~ /$re/) {
			    push(@skippedList,$file);
			    $excluded=1;
			    $verbose>5&& 
				print STDERR "\t excluded due to match with "
				    . $re . "\n";
			    next;
			}
		    }
		    if ( $excluded ) {
			foreach my $re (@includeList) {
			    if ($file =~ /$re/) {
				$excluded=0;
				$verbose>5&& 
				    print STDERR "\t NOT excluded due to " .
					"match with $re\n";
				next;
			    }
			}
		    }
		    ($excluded) ||
			MakeHash($file,1); #recurseFlag is always true here
		}
		closedir(DIR1);
	    }
	} elsif (-f _) {
	    $verbose>8 && print STDERR "regular file\n";
	    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime)
		= stat(_);
	    
	    ($nlink>1) &&
		($spec{"file"}{$filename}{"inode"} = $ino);

	    readFromHashSave( \%{$spec{"file"}},$filename, 
			      $size,$mtime,"",$mode);
	} else {
	    $verbose && print STDERR 
		"$filename is not a regular file, directory or symlink.\n" .
		    "\tSkipping...\n";
	}
##    } else {
##	  print STDERR "$filename does not exist!\n";
##    }
} # MakeHash

sub readFromHashSave {
    my ($specr, $filename, $size, $mtime, $target, $mode) = @_;
    if ( checkHashMiss($filename,$size,$mtime) ) {
	$$specr{$filename}{"mod"} = 1;
	$$specr{$filename}{"size"} = $size;
	$$specr{$filename}{"time"} = $mtime;
	if ( $target eq "" ) {
	    $$specr{$filename}{"csum"} = CheckSum($filename,$mode);
	} else {
	    $$specr{$filename}{"csum"} = checkSumString($target);
	}
    } else {  # hashSave hit
	$$specr{$filename}{"mod"} = 0;
	$$specr{$filename}{"size"} = $size;
	$$specr{$filename}{"time"} = $mtime;
	$$specr{$filename}{"csum"} = $csumHashSave{$filename};
    }
} # readFromHashSave

sub checkHashMiss { 
    # return 1 on hash miss, 0 otherwise
    my ($specname,$size,$mtime) = @_;
    if (!exists $timeHashSave{$specname} ||
	$sizeHashSave{$specname} != $size ||
	$timeHashSave{$specname} != $mtime) {
	#
	#  do the checksum if the old hash data is not up to date
	#
	$hashMiss++;
	($verbose>3) &&	print STDERR "Hash miss: $specname\n";
	($verbose>3) &&	print STDERR "\tsaved   time: ".
	    localtime($timeHashSave{$specname}) . "\n";
	($verbose>3) &&	
	    print STDERR "\tcorrect time: ". localtime($mtime) . "\n";
	($verbose>3) &&
	    print STDERR "\tsaved   size: " . $sizeHashSave{$specname}."\n";
	($verbose>3) &&	
	    print STDERR "\tcorrect size: $size\n";
	return 1;
    }
    return 0;
} # checkHashMiss
    
sub RemoveDirEntries {
#
# given a directory name, remove all entries in a hash which start with the 
# directory name.
#
    my ($hashref,$name) = @_;

    $verbose>1 && print STDERR "RemoveDirEntries: processing $name\n";
    foreach my $type ("file", "symlink", "hardlink", "dir") {
	foreach my $entry (keys %{$$hashref{$type}}) {
	    if ($entry =~ /^\Q$name\E(\/|$)/) { #FIX: OS dependency: trailing /
		$verbose>5 && 
		    print STDERR "RemoveDirEntries: deleting $entry\n";
		delete $$hashref{$type}{$entry};
	    }
	}
    }
} # RemoveDirEntries


##sub CheckSum {
##    my $filename = shift;
##    my $filemode = shift;
##    
##    chmod(0700,"$filename");    #allow
##    $filename =~ s#^(\s)#./$1#; #protect leading whitespace
##    open(FILE,"< $filename\0") || 
##	  fsDie("CheckSum: cannot open file: $filename\n");
##    my $checksum=0;
##    while (<FILE>) {
##	   $checksum += unpack("%16C*", $_);
##    }
##    close(FILE);
##    chmod($filemode,"$filename"); #restore permissions
##    
##    $checksum %= 65536;
##    return $checksum;
##}

##
## checksum code taken from cksum.c, by Q. Frank Xia, qx@math.columbia.edu.
##

sub checkSumPerl {
    my $filename = shift;
    if ( !open(FILE,"< $filename\0") ) {
	setError("CheckSum: cannot open file: $filename\n");
	return 0;
    }
    
    my $crc = 0;
    my $length = 0;
    my $buflen = 1<<16;
    my $buf;
    while ((my $bytes_read = sysread (FILE,$buf,$buflen)) > 0) {
	$length += $bytes_read;
	for (my $i=0 ; $i<$bytes_read ; $i++) {
	    my $c = ord( substr($buf,$i,1) );
	    $crc = ($crc << 8) ^ 
		$crctab[(($crc >> 24) ^ $c) & 0xFF];
	}
    }
    close(FILE);
	
    my $bytes_read = $length;
    while ( $bytes_read > 0 ) {
	$crc = ($crc << 8) ^ $crctab[(($crc >> 24) ^ $bytes_read) & 0xFF];
	$bytes_read >>= 8;
    }
    
    $crc = ~$crc & 0xFFFFFFFF;
    return $crc;
} # checkSumPerl

sub checkSumString {
    my $string = shift;
    my $crc = 0;
    my $length = 0;

    for (my $i=0 ; $i<length( $string ) ; $i++) {
	my $c = ord( substr($string,$i,1) );
	$crc = ($crc << 8) ^ 
	    $crctab[(($crc >> 24) ^ $c) & 0xFF];
    }
	
    my $bytes_read = length( $string );
    while ( $bytes_read > 0 ) {
	$crc = ($crc << 8) ^ $crctab[(($crc >> 24) ^ $bytes_read) & 0xFF];
	$bytes_read >>= 8;
    }
    
    $crc = ~$crc & 0xFFFFFFFF;
    ($verbose>2) && print STDERR "CheckSumString: $crc\n";
    return $crc;
} # checkSumString

sub quoteShell {
    my $string = shift;
    $string =~ s/([ \t*?<>&\$"'`|{}[\]\\()])/\\$1/g;
    return $string;
} # quoteShell    

sub CheckSum {
    # special values of checkSumProg: 
    #   ""  :  use built-in (slow Perl) checksum
    # "none":  no checksum
    #
    my $filename = shift;
    my $filemode = shift;
    
    $checkSumProg eq "none" && return 0;

    chmod(0666,"$filename");    #allow
    $filename =~ s#^(\s)#./$1#; #protect leading whitespace

    my $ crc = 0;
    if ( $checkSumProg ne "" ) {
	my $spaceProtected = quoteShell($filename);
	my $ret = `$checkSumProg $spaceProtected`;
	($crc) = split(" ",$ret);
    } else {
	$crc = checkSumPerl($filename);
    }
    chmod($filemode,"$filename"); #restore permissions
    ($verbose>2) && print STDERR "CheckSum: $filename: $crc\n";

    return $crc;
} # CheckSum

sub initCheckSum {
@crctab =
(
  0x0,
  0x04C11DB7, 0x09823B6E, 0x0D4326D9, 0x130476DC, 0x17C56B6B,
  0x1A864DB2, 0x1E475005, 0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6,
  0x2B4BCB61, 0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD,
  0x4C11DB70, 0x48D0C6C7, 0x4593E01E, 0x4152FDA9, 0x5F15ADAC,
  0x5BD4B01B, 0x569796C2, 0x52568B75, 0x6A1936C8, 0x6ED82B7F,
  0x639B0DA6, 0x675A1011, 0x791D4014, 0x7DDC5DA3, 0x709F7B7A,
  0x745E66CD, 0x9823B6E0, 0x9CE2AB57, 0x91A18D8E, 0x95609039,
  0x8B27C03C, 0x8FE6DD8B, 0x82A5FB52, 0x8664E6E5, 0xBE2B5B58,
  0xBAEA46EF, 0xB7A96036, 0xB3687D81, 0xAD2F2D84, 0xA9EE3033,
  0xA4AD16EA, 0xA06C0B5D, 0xD4326D90, 0xD0F37027, 0xDDB056FE,
  0xD9714B49, 0xC7361B4C, 0xC3F706FB, 0xCEB42022, 0xCA753D95,
  0xF23A8028, 0xF6FB9D9F, 0xFBB8BB46, 0xFF79A6F1, 0xE13EF6F4,
  0xE5FFEB43, 0xE8BCCD9A, 0xEC7DD02D, 0x34867077, 0x30476DC0,
  0x3D044B19, 0x39C556AE, 0x278206AB, 0x23431B1C, 0x2E003DC5,
  0x2AC12072, 0x128E9DCF, 0x164F8078, 0x1B0CA6A1, 0x1FCDBB16,
  0x018AEB13, 0x054BF6A4, 0x0808D07D, 0x0CC9CDCA, 0x7897AB07,
  0x7C56B6B0, 0x71159069, 0x75D48DDE, 0x6B93DDDB, 0x6F52C06C,
  0x6211E6B5, 0x66D0FB02, 0x5E9F46BF, 0x5A5E5B08, 0x571D7DD1,
  0x53DC6066, 0x4D9B3063, 0x495A2DD4, 0x44190B0D, 0x40D816BA,
  0xACA5C697, 0xA864DB20, 0xA527FDF9, 0xA1E6E04E, 0xBFA1B04B,
  0xBB60ADFC, 0xB6238B25, 0xB2E29692, 0x8AAD2B2F, 0x8E6C3698,
  0x832F1041, 0x87EE0DF6, 0x99A95DF3, 0x9D684044, 0x902B669D,
  0x94EA7B2A, 0xE0B41DE7, 0xE4750050, 0xE9362689, 0xEDF73B3E,
  0xF3B06B3B, 0xF771768C, 0xFA325055, 0xFEF34DE2, 0xC6BCF05F,
  0xC27DEDE8, 0xCF3ECB31, 0xCBFFD686, 0xD5B88683, 0xD1799B34,
  0xDC3ABDED, 0xD8FBA05A, 0x690CE0EE, 0x6DCDFD59, 0x608EDB80,
  0x644FC637, 0x7A089632, 0x7EC98B85, 0x738AAD5C, 0x774BB0EB,
  0x4F040D56, 0x4BC510E1, 0x46863638, 0x42472B8F, 0x5C007B8A,
  0x58C1663D, 0x558240E4, 0x51435D53, 0x251D3B9E, 0x21DC2629,
  0x2C9F00F0, 0x285E1D47, 0x36194D42, 0x32D850F5, 0x3F9B762C,
  0x3B5A6B9B, 0x0315D626, 0x07D4CB91, 0x0A97ED48, 0x0E56F0FF,
  0x1011A0FA, 0x14D0BD4D, 0x19939B94, 0x1D528623, 0xF12F560E,
  0xF5EE4BB9, 0xF8AD6D60, 0xFC6C70D7, 0xE22B20D2, 0xE6EA3D65,
  0xEBA91BBC, 0xEF68060B, 0xD727BBB6, 0xD3E6A601, 0xDEA580D8,
  0xDA649D6F, 0xC423CD6A, 0xC0E2D0DD, 0xCDA1F604, 0xC960EBB3,
  0xBD3E8D7E, 0xB9FF90C9, 0xB4BCB610, 0xB07DABA7, 0xAE3AFBA2,
  0xAAFBE615, 0xA7B8C0CC, 0xA379DD7B, 0x9B3660C6, 0x9FF77D71,
  0x92B45BA8, 0x9675461F, 0x8832161A, 0x8CF30BAD, 0x81B02D74,
  0x857130C3, 0x5D8A9099, 0x594B8D2E, 0x5408ABF7, 0x50C9B640,
  0x4E8EE645, 0x4A4FFBF2, 0x470CDD2B, 0x43CDC09C, 0x7B827D21,
  0x7F436096, 0x7200464F, 0x76C15BF8, 0x68860BFD, 0x6C47164A,
  0x61043093, 0x65C52D24, 0x119B4BE9, 0x155A565E, 0x18197087,
  0x1CD86D30, 0x029F3D35, 0x065E2082, 0x0B1D065B, 0x0FDC1BEC,
  0x3793A651, 0x3352BBE6, 0x3E119D3F, 0x3AD08088, 0x2497D08D,
  0x2056CD3A, 0x2D15EBE3, 0x29D4F654, 0xC5A92679, 0xC1683BCE,
  0xCC2B1D17, 0xC8EA00A0, 0xD6AD50A5, 0xD26C4D12, 0xDF2F6BCB,
  0xDBEE767C, 0xE3A1CBC1, 0xE760D676, 0xEA23F0AF, 0xEEE2ED18,
  0xF0A5BD1D, 0xF464A0AA, 0xF9278673, 0xFDE69BC4, 0x89B8FD09,
  0x8D79E0BE, 0x803AC667, 0x84FBDBD0, 0x9ABC8BD5, 0x9E7D9662,
  0x933EB0BB, 0x97FFAD0C, 0xAFB010B1, 0xAB710D06, 0xA6322BDF,
  0xA2F33668, 0xBCB4666D, 0xB8757BDA, 0xB5365D03, 0xB1F740B4
);
    if ( $checkSumProg ne "" && $checkSumProg ne "none" ) {
	my $run = `$checkSumProg $0`;
	my ($retProg) = split(" ",$run);
	my $retPerl = checkSumPerl($0);
	if ( $retProg != $retPerl ) {
	    print STDERR 
		"external checksum ($checkSumProg) returned incorrect value.";
	    print STDERR "\n\tusing internal (slow) routine.\n";
	    $checkSumProg = "";
	}
    }
	
$verbose>8 && print STDERR "crctab initialized.\n";
}

sub fsDie {
    my $message = shift;

    close($nethandle_in);
    close($nethandle_out);
    die $message;
}

sub getMyIP {
    my $remhost = shift;

    my $port = "telnet(23)";
    my $remote = IO::Socket::INET->new( Proto     => "tcp",
					PeerAddr  => $remhost,
					PeerPort  => $port,
					);
    unless ($remote) { 
	die "cannot connect to host $remhost using port $port\n";
    }
    $remote->autoflush(1);

##    my $peername = $remote->peerhost;
##    print "connected to $peername\n";
##    
    my $ipNum = $remote->sockhost;
    
    close $remote;
    return $ipNum;
}

sub findUnusedPort {
    # check that  host $host is not listening on port $port 
    # increment port until one is found.
    my ($host, $port) = @_;
    my $oldW = $^W;
    $^W=0;
    while (1) {
	my $nethandle = IO::Socket::INET->new(Proto     => "tcp",
					      PeerAddr  => $host,
					      PeerPort  => $port,
					      Timeout   => 2);
	if ( $nethandle ) {
	    close($nethandle);
	    print "port $port on $remhost is in use. Incrementing port.\n";
	    $port++;
	} else {
	    ($verbose>2) && 
		print "failure connecting to $host:$port. All is good.\n";
	    last;
	}
    }
    $^W=$oldW;
    return $port;
}

sub setError {
    $errorMsg = shift;
    $errorMsg eq "" || print STDERR "setError: $errorMsg\n";
}

sub checkError {
    $noErrorCheck && return;
    sendLine("get-error");
    my $remErr = getLine();
    ($remErr eq "") || print STDERR "Error on server: $remErr\n";
    ($errorMsg eq "") || print STDERR "Error: $errorMsg.\n";
    $errorMsg = "";
}

sub dbClientCopyFrom {
    # this does not deal intelligently with simple insertions
    # $same: the files have same size and checksum. Should only
    # happen if checksums are turned off and times differ
    my ($file,$same) = @_;

    $verbose && 
	print STDERR "Copying $file from $remhost.\n";

    $dbDelim = "";
    foreach my $entry ( keys %dbFiles ) {
	$verbose>2 &&
	    print STDERR "dbClientCopyFrom: file: >$file<   entry: >$entry<\n";
	if ( $file =~ /$entry/ ) {
	    $verbose>1 &&
		print STDERR "dbClientCopyFrom: database file: $file\n";
	    $dbDelim = $dbFiles{$entry};
	    last;
	}
    }
    $verbose>2 && print STDERR "dbDelim: $dbDelim\n";
##    if ( length($dbDelim) == 0 ) {
##	  clientCopyFrom($file,$file);
##	  return;
##    }

    $intCount=0;
    $SIG{'INT'}= sub { $intCount++; 
		     $intCount>2 && clientHandler("INT"); 
		     print STDERR "dbClientCopyFrom: aborting...\n"};

    sendLines("db-make-csum",$file);
    NsendRaw( \$dbDelim );
    my ( @ldbCSum, @rdbCSum );
    dbCheckSum($file,\@ldbCSum);
    sendLine("db-get-csum");
    my $rSize = getLine();
    getArray( \@rdbCSum, $rSize );
    sendLine("db-copy-from");

    $SIG{'PIPE'} = eval {sub { $setDate && utime 0,0, $file; 
			     clientHandler("INT"); }};
    if ( !dbCopyFrom( $file , \@rdbCSum , \@ldbCSum ) && !$same ) {
	print STDERR "dbClientCopyFrom: Recopy forced.\n";
	clientCopyFrom($file,$file);
    }
    $SIG{'INT'}=$SIG{'PIPE'}=  'clientHandler';

} # dbClientCopyFrom

sub dbClientCopyTo {
    # this does not deal intelligently with simple insertions
    # $same: the files have same size and checksum. Should only
    # happen if checksums are turned off and times differ
    my ($file,$same) = @_;

    $verbose && 
	print STDERR "Copying $file to $remhost.\n";

    $dbDelim = "";
    foreach my $entry ( keys %dbFiles ) {
##	print STDERR "dbClientCopyTo: file: >$file<   entry: >$entry<\n";
	if ( $file =~ /$entry/ ) {
	    $verbose>1 &&
		print STDERR "dbClientCopyTo: database file: $file\n";
	    $dbDelim = $dbFiles{$entry};
	    last;
	}
    }
##    print STDERR "dbDelim: $dbDelim\n";
##    if ( length($dbDelim) == 0 ) {
##	  clientCopyTo($file,$file);
##	  return;
##    }

    sendLines("db-make-csum",$file);
    NsendRaw( \$dbDelim );
    my ( @ldbCSum, @rdbCSum );
    dbCheckSum($file,\@ldbCSum);
    sendLine("db-send-csum " . scalar(@ldbCSum)); sendArray( \@ldbCSum );
    sendLine("db-copy-to");
    $intCount=0;
    $SIG{'INT'}= sub { $intCount++; 
		     $intCount>2 && clientHandler("INT"); 
		     print STDERR "dbCopyTo: aborting...\n"};
    if ( !dbCopyTo($file) && !$same ) {
	print STDERR "dbClientCopyTo: Recopy forced.\n";
	clientCopyTo($file,$file);
    }
} # dbClientCopyTo

my $dbCurRecord="";
my $dbCurRecordNum=-1;
my $dbCopyCancel=0;


my $dbFileHandle;
my $dbFilename;
my $dbFilemode;
my $dbFileuid;
my $dbFilegid;

sub dbOpen {
    $dbFilename = shift;

    my ($dev,$ino,$mode,$nlink,$uid,$gid) = stat($dbFilename);
    $dbFilemode = $mode;
    $dbFileuid = $uid;
    $dbFilegid = $gid;
    chmod(0666,$dbFilename); #allow access

    $dbCurRecordNum=-1;
    $dbFilename =~ s#^(\s)#./$1#; #protect whitespace
    if (!open( dbFILE , "< $dbFilename")) {
	print STDERR "dbOpen: error opening file $dbFilename\n";
	return 0;
    }
    $dbFileHandle = *dbFILE;
} # dbOpen
    

sub dbClose {
    close( $dbFileHandle );
    chmod($dbFilemode,"$dbFilename\0"); #reset permissions
    $setOwn && chown($dbFileuid,$dbFilegid,"$dbFilename\0");
} # dbClose
    

sub dbCopyFrom {
    my $filename = shift;
    my $rdbCSum = shift;
    my $ldbCSum = shift;

    $dbCurRecordNum=-1;

    $filename =~ s#^(\s)#./$1#; #protect whitespace
    my ($dev,$ino,$oldmode,$nlink,$uid,$gid) = stat($filename);
    chmod(0666,$filename);
    link($filename, $tmpFilename);
    Exists($filename) && makeBackup( $filename );
    dbOpen( $tmpFilename );
    my $lfileRef;
    if (!openLocked(\$lfileRef,$filename,">")) {
	print STDERR "dbCopyFrom: error opening file $filename\n";
	return 0;
    }
    my $lSize = scalar(@$ldbCSum);
    my $rSize = scalar(@$rdbCSum);
    my $minSize = ($lSize<$rSize) ? $lSize : $rSize;
    my $maxSize = ($lSize<$rSize) ? $rSize : $lSize;
    my $record;
    my $recCopied=0;
    for (my $i=0 ; $i<$minSize ; $i++) {
	$intCount && goto readEnd;
	$verbose>8 &&
	    print STDERR "dbCopyFrom: $i: $$ldbCSum[$i] $$rdbCSum[$i]\n";
	if ( $$ldbCSum[$i] != $$rdbCSum[$i] ) {
	    $recCopied=1;
	    sendLine( $i );
	    NgetRaw( \$record );
	    $record || goto readEnd;
	    dbWriteRecord( $lfileRef , \$record );
	} else {
	    dbReadRecord( $dbFileHandle , $i, \$record );
	    dbWriteRecord( $lfileRef , \$record );
	}
	
    }
    for (my $i=$minSize ; $i<$maxSize ; $i++) {
	$verbose>8 &&
	    print STDERR "dbCopyFrom: $i: intCount: $intCount\n";
	$recCopied=1;
	$intCount && last;
	sendLine($i);
	NgetRaw( \$record );
	$record || last;
	dbWriteRecord( $lfileRef , \$record );
    }
  readEnd:
	
    sendLine("-1"); # this terminates dbCopyTo on remhost
    dbClose();
    closeLocked($lfileRef);

    unlink($tmpFilename);
    #
    # set date and mode
    #
    my $filetime = getLine();
    $intCount && ( $filetime=0 );
    $setDate && utime $filetime, $filetime, $filename;

    my  $filemode = getLine();
    $setPerms || ($filemode = $oldmode);
    if ( $setOwn ) {
	my  $fileuid = getLine();
	my  $filegid = getLine();
	chown($fileuid,$filegid,"$filename");
    }


    {
	my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$filesize,$atime,$filetime)
	    = stat($filename);
	#
	# update the local hashSave info .... need to think about this...
	#
	#  --don't really want to save temporary (used for merging) files
	# add: could check if the hash already exists, and then add
	$timeHashSave{$filename} = $filetime;
	$sizeHashSave{$filename} = $filesize;
	$csumHashSave{$filename} = CheckSum($filename,$filemode);

    }
    $recCopied || $verbose>1 &&
	print STDERR "dbCopyFrom: hash collision detected while copying\n" .
	    "\t$filename.\n";
    return $recCopied;
}   ## dbCopyFrom

sub dbReadRecord {
    #
    # assume file open - where is current position?
    #
    my $file = shift;
    my $recordNum = shift;
    my $record = shift;
    
    $verbose>8 && 
	printf STDERR ("dbReadRecord: recordNum: %d  curRecordNum: %d\n",
		       $recordNum,$dbCurRecordNum);

    ( $recordNum == $dbCurRecordNum ) && 
	( $$record = $dbCurRecord );

    if ( $recordNum < $dbCurRecordNum ||
	 $dbCurRecordNum == -1         ) {
	$dbCurRecordNum = -1;
	sysseek($file,0,0); #rewind
    }
    my $ret=1;
    for (my $i=$dbCurRecordNum ; $i<$recordNum ; $i++) {
	$ret=dbReadOneRecord( $file, $record );
	$verbose>8 && 
	    printf STDERR ("dbReadRecord: ret: %d  record %d: size %d \n",
				     $ret,$i+1,length($$record));
    }
    # FIX: what if eof happens bbefore we get to $recordNum?
    $dbCurRecord = $$record;
    $dbCurRecordNum  = $recordNum;
    return $ret; ##eof($file) ? 0 : $ret;
} # dbReadRecord

sub dbReadOneRecord {
    #
    # either reads a specified block size or reads up til a delimiter
    # delimiter aspect is brain-dead- it must exist on a line of its own.
    #
    # read a block of size dbBlockSize, and set record to the first
    # region up to and including the database delimiter. Return this block
    # in record. Position the file immediately after this block. Note that
    # sysread/sysseek must be useed.
    #
    # note that zero-sized records are not allowed
    #
    my $file = shift;
    my $recordr = shift;

#    print "$dbSizeDelim $dbBlockSize\n";
    #
    # 
#
    my $record = "";
    $$recordr = "";
    while ( 1 ) {
	my $block;
	my $size = sysread( $file, $block , $dbBlockSize);
        if ( $dbDelim ) { 
	    $record .= $block;
	    if ( $record =~ /$dbDelim/ ) {
		$$recordr = $`. $dbDelim;
		my $rewindAmount = length($');
#		print "rewindAmount: $rewindAmount $`\n";
		( $rewindAmount == 0 ) &&
		    return 0;
		# rewind to just after dbDelim 
		if ( $size < $dbBlockSize ) {
		    sysseek( $file , -$rewindAmount , 2 );
		} else {
		    sysseek( $file , -$rewindAmount , 1 );
		}
		return 1;
	    }
	    if ( $size < $dbBlockSize ) {
		$$recordr = $record;
		return 0;
	    }
	} else {
	    $$recordr = $block;
	    ( $size < $dbBlockSize ) &&
		return 0;
	    return 1;
	}
    }
} # dbReadOneRecord

sub dbWriteRecord {
    #
    # assume file open - write at EOF
    #
    my $file = shift;
    my $record = shift;

    print $file $$record;
} # dbWriteRecord
    

sub dbCopyTo {
    #
    # copy local file to remote, given the arguments below
    #
    my $filename = shift; #local filename

    my ($dev,$ino,$filemode,$nlink,$uid,$gid,$rdev,$filesize,$atime,$filetime)
	= stat($filename);
    #
    # read local file off the disk
    #
    dbOpen( $filename );
    my $field=0;
    my $recCopied=0;
    while ( ($field=getLine()) >= 0 ) {
	$recCopied=1;
	my $record;
	dbReadRecord( $dbFileHandle, $field, \$record);
	$intCount && ( $record = "" );
	NsendRaw( \$record );
    }
    dbClose();

    #send file time;
    $intCount && ( $filetime=0 );
    sendLine($filetime);
    
    # send permissions
    sendLine($filemode);
    $setOwn && ( sendLine($uid) , sendLine($gid) );
    syncHash( $filename );
    $recCopied || $verbose>1 &&
	print STDERR "dbCopyTo: hash collision detected while copying\n" .
	    "\t$filename.\n";
    return $recCopied;
   
} # dbCopyTo

sub dbCheckSum {
    my $filename = shift;
    my $checksumA = shift;

    my ($dev,$ino,$filemode) = stat($filename);
    dbOpen( $filename );
    my $cnt=0;
    my $size=0;

    my $record;
    while (  dbReadRecord( $dbFileHandle , $cnt , \$record ) ) {
#	print "dbCheckSum: $cnt: $record\n";
#	$$checksumA[ $cnt ] = unpack("%16C*", $record);
	$$checksumA[ $cnt ] = myCheckSum( \$record );
	$cnt++;
    }
    # final record
#    $record && ( $$checksumA[ $cnt ] = unpack("%16C*", $record) );
    $record && ( $$checksumA[ $cnt ] = myCheckSum( \$record) );
	
    dbClose();
    $verbose>3&& print STDERR "number of records: $cnt\n";
    $size = @$checksumA;

##    print "checksum for message $cnt: $checksum\n";
##    print "there are $cnt messages in $filename\n";
} # dbCheckSum

#sub myCheckSum {
#    #
#    # catches transposed lines, but not transposed characters
#    #
#    my $bufRef = shift;
#    my $bufSize=41;
#    my $maxCS=2**20;
#    my $format = sprintf("%%32C*",$bufSize);
#    
#    my $cs = 0;
#    my $rc=1;
#    for (my $i=0 ; $i<length($$bufRef)/$bufSize ; $i++) {
#	 my $ss = substr($$bufRef,$i*$bufSize,$bufSize);
#	 $cs = int($cs +  ($i+1) * unpack("%32C*",$ss));
#    }
#    $cs %= $maxCS;
#    return $cs;
#}
sub myCheckSum {
    my $bufRef = shift;
    my $bufSize=41;
    my $maxCS=2**20;
    my $format = sprintf("%%32C*",$bufSize);
    my $mask = join('',('A' .. 'Z', 'a' .. 'z'));
    
    my $cs = 0;
    my $rc=1;
    for (my $i=0 ; $i<length($$bufRef)/$bufSize ; $i++) {
	my $ss = substr($$bufRef,$i*$bufSize,$bufSize);
	$ss ^= $mask;
	$cs += ($i+1)*unpack("%C*",$ss);
	$cs   %= $maxCS;
    }
    return $cs;
} # myCheckSum

sub ignoreSpec {
    my ($entry,$type,$spec,$Rspec,$ignFile) = @_;
    #
    # add given entry of spectype type to skipFile or to rcFile (specified by
    # ignFile). All entries in spec and Rspec which match given regexp are 
    # removed. As a side effect, the global array current keys is
    # updated with the keys from spec.
    #
    if ( $ignFile eq "skip") {
	$verbose && 
	    print STDERR "ignoreSpec: adding $type $entry to $skipFilename\n";
	use File::Basename;
	my ($file,$parentDir) = fileparse($entry);
	$parentDir .= (($parentDir =~ m!/$!) ? '' : '/');
	my $skipFile = $parentDir  . $skipFilename;
	#FIX: the check doesn't work: need to get path relative to root
	if (exists $$Rspec{"file"}{$skipFile}{"csum"} &&
	    $$Rspec{"file"}{$skipFile}{"csum"} != 
	    $$spec{"file"}{$skipFile}{"csum"}           ) {
	    print "ignoreSpec: " . $skipFile . 
		" exists on remhost and differs.\n";
	    print "\tcontinuing will cause loss of changes of remote version.\n";
	    my $ans= readInteractive("c:s","s",
				     "(c)   continue. Overwrite remote skip file",
				     "(s)   don't modify local skip file");
	    $ans =~ /^s/ && return;
	}
	# protect regex chars
	$file = quotemeta($file);
	# add $ to end
	my $re = $file . "\$";
	print "regexp to add to skipFile: [$re] ";
	my $rre = readFlushedStdin();
	(length($rre)>2) && ( $re = $rre );	    
	# append to skipfilename
	open(SFILE,">>$skipFile") || warn("error opening $skipFile");
	print SFILE "$re\n";
	close(SFILE);
	# cp skipfilename to remhost
	clientCopyTo($skipFile,$skipFile);
	delete $$spec{"file"}{$skipFile};
	delete $$Rspec{"file"}{$skipFile};
	delete $$spec{$type}{$entry};
	delete $$Rspec{$type}{$entry};
    } else { # rcFile
	$verbose && 
	    print STDERR "ignoreSpec: adding $entry to $cmd_file\n";
	$entry =~ s/^\Q$rootdir\E//;   #make path relative to root
	my $re = "^" . quotemeta($entry) . "\$";
	print "regexp to add to exclude list: [$re] ";
	my $rre = readFlushedStdin();
	chomp( $rre );
	(length($rre)>2) && ( $re = $rre );	    

	# append to rcFile
	open(SFILE,">>$cmd_file") || warn("error opening $cmd_file");
	print SFILE "\npush(\@excludeList, '$re');\n";
	close(SFILE); 
	foreach my $type ("file", "hardlink", "symlink") {
	    foreach my $name ( keys %{$$spec{$type}} ){
		#($name =~ /$re/) && print STDERR "skipping: $name\n";
		($name =~ /$re/) && delete $$spec{$type}{$name};
	    }
	    foreach my $name ( keys %{$$Rspec{$type}} ){
		$name =~ m/$re/ && delete $$Rspec{$type}{$name};
	    }
	}
	foreach my $name ( keys %{$$spec{"dir"}} ) {
	     $name =~ m/$re/ &&	RemoveDirEntries($spec,$name);
	 }
	foreach my $name ( keys %{$$Rspec{"dir"}} ) {
	     $name =~ m/$re/ &&	RemoveDirEntries($Rspec,$name);
	 }
    }
    @currentKeys = sort(keys( %{$$spec{$type}}));
} # ignoreSpec


sub checkDiskSpace {
    my $reserveSize=shift;

    $verbose>3 && 
	printf(STDERR
	       "checkDiskSpace: checking for %dmb of free disk space on %s\n",
	       $reserveSize, $rootdir);

    $reserveSize *= 1024*1024; # reserveSize specified in mb

    my $blockSize = 1024*1024; # 1mb

    use Fcntl;
    use POSIX qw(tmpnam);

    # try new temporary filenames until we get one that didn't already
    # exist;  the check should be unnecessary, but you can't be too careful

    use Fcntl;

    my $name = sprintf("%s/fsync-%d-%d-0000", $rootdir, $$, time());
    local *FH;
    my $count=0;
    until (defined(fileno(FH)) || $count++ > 100) {
	$name =~ s/-(\d+)$/"-" . (1 + $1)/e;
	sysopen(FH, $name, O_WRONLY|O_EXCL|O_CREAT);
    }

    if ( !defined(fileno(FH)) ) {
	print "checkDiskSpace: could not create a temporary file.\n";
	return
    }

    $verbose>3 && print STDERR "checkDiskSpace: tmpname: $name\n";

    # install atexit-style handler so that when we exit or die,
    # we automatically delete this temporary file
    
    my $failed=0;
    my $block = "x" x $blockSize;
    for (my $size= $reserveSize ; $size>0; $size-=$blockSize) {
	my $sizeWritten = syswrite(FH,$block,$size);
	if ( $sizeWritten < $size && 
	     $sizeWritten<$blockSize ) {
	    $failed=1;
	    last;
	}
    }
    close(FH);

    if ( !$failed ) {
	sysopen(FH,$name, O_RDONLY) || die("error opening $name: $!");
	for (my $size=$reserveSize; $size>0; $size-=$blockSize) {
	    my $sizeRead = sysread(FH,$block,$blockSize);
	    if ( $sizeRead != $reserveSize && 
		 $sizeRead != $blockSize ) {
		print "Diskspace check failed: $sizeRead\n";
		$failed=1;
		last;
	    }
	}
	close(FH);
    }
    unlink($name) or die "Couldn't unlink $name : $!";
    if ( $failed ) {
	$verbose>3 && 
	    print STDERR "checkDiskSpace: check failed\n";
	return 0;
    } else { 
	$verbose>3 && 
	    print STDERR "checkDiskSpace: check passed\n";
	return 1;
    }
} # checkDispSpace

