#!/usr/bin/gawk -f
#


# ---------------------------------------------------------------------
# Part I - FTP client copied from ftpclient script.
#

function noctrl(line) {
	sub(/[ \t\r\n]+$/, "", line);
	return (line);
	}

function cfgets(ftpd,   line) {
	if (ftpd |& getline line > 0) {
		line = noctrl(line);
		if (debug != 0)
			printf (">>> %s\n", line) >>STDERR;

		return (line);
		}

	return ("\001");
	}

function cfputs(ftpd, line) {
	line = noctrl(line);
	if (debug != 0)
		printf ("<<< %s\n", line) >>STDERR;

	printf ("%s\r\n", line) |& ftpd;
	fflush();

	return (0);
	}

function cfputc(ftpd, cmd, arg, result,   k, rc, line, fatal) {
	if (cmd != "") {
		line = cmd;
		if (arg != "")
			line = cmd " " arg;

		cfputs(ftpd, line);
		}

	rc = 0;
	while (1) {
		if ((line = cfgets(ftpd)) == "\001")
			break;

		if (rc == 0)
			rc = line+0;

		if (line+0 == rc  &&  line ~ /^[0-9]/) {
			if (length(line) <= 3  ||  substr(line, 4, 1) == " ")
				break;
			}
		}

	fatal = 1;
	if (result < 0) {
		result = -result;
		fatal = 0;
		}

	rc = toupper(rc);
	if (result != ""  &&  rc != result) {
		if (fatal == 1) {
			printf ("%s: protocol error: %s\n", program, line) >>STDERR;
			exit (1);
			}
		}

	return (line);
	}



function doport(ftpd, options,   local, x, high, low) {

	portcmd = sprintf ("connect -a %s: %s", interface, options);
	portcmd |& getline local;
	split(local, x, ":");

	high = int(x[2] / 256);
	low = x[2] - high * 256;

	local = x[1] "," high "," low;
	gsub(/\./, ",", local);

	cfputc(ftpd, "PORT", local, 200);
	return (portcmd);
	}

function dolist(ftpd, cmd, dir) {
	cfputc(ftpd, "TYPE", "A", 200);
	portcmd = doport(ftpd, "");

	cfputc(ftpd, (cmd != "NLST")? "LIST": "NLST", dir, 150);
	portcmd |& getline line;


	while (portcmd |& getline line) {
		noctrl(line);
		printf ("%s\n", line);
		}

	close (portcmd);
	cfputc(ftpd, "", "", 226);

	return (0);
	}

function doretr(ftpd, mode, remotename, localname) {
	if (localname == "")
		localname = remotename;

	cfputc(ftpd, "TYPE", mode, 200);
	portcmd = doport(ftpd, mode == "A"? "": sprintf("-w '%s'", localname));

	line = cfputc(ftpd, "RETR", remotename, 150);
	portcmd |& getline line;
	if (mode != "A") {
		portcmd |& getline line;
		}
	else {
		while (portcmd |& getline line) {
			noctrl(line);
			printf ("%s\n", line) >localfile;
			}

		close (localfile);
		}

	close (portcmd);
	cfputc(ftpd, "", "", 226);

	return (0);
	}

function dostor(ftpd, mode, localname, remotename) {
	if (remotename == "")
		remotename = localname;

	cfputc(ftpd, "TYPE", mode, 200);
	portcmd = doport(ftpd, sprintf ("-r '%s'", localname));

	cfputc(ftpd, "STOR", remotename, 150);
	portcmd |& getline line;

	portcmd |& getline line;

	close (portcmd);
	cfputc(ftpd, "", "", 226);

	return (0);
	}

function dorename(ftpd, oldname, newname) {
	if (cfputc(ftpd, "RNFR", oldname, 350) + 0 == 350)
		cfputc(ftpd, "RNTO", newname, 250);

	return (0);
	}

function dodele(ftpd, remotename) {
	cfputc(ftpd, "DELE", remotename, 250);
	return (0);
	}

function domkd(ftpd, dir) {
	cfputc(ftpd, "MKD", dir, 257);
	return (0);
	}

function dormd(ftpd, dir) {
	cfputc(ftpd, "RMD", dir, 250);
	return (0);
	}

function docwd(ftpd, dir) {
	cfputc(ftpd, "CWD", dir, 250);
	return (0);
	}

function dopwd(ftpd, dir,   line) {
	line = cfputc(ftpd, "PWD", "", 257);
	if (line + 0 == 257) {
		sub(/^[^ ]+ +/, "", line);
		if (substr(line, 1, 1) != "\"")
			sub(/ .*$/, "", line);
		else {
			line = substr(line, 2);
			sub(/".*$/, "", line);
			}

		printf ("%s\n", line);
		}

	return (0);
	}


# ---------------------------------------------------------------------
# Part II - Syncing
#

function excludefile(file) {
	if (file ~ conflictchar  ||  file ~ /\t/)
		return (1);
	else if (file == "."  ||  file == ".."  ||  file ~ /\.swp$|~$/)
		return (1);
	else if (file ~ /;'"/)
		return (1);
	else if (file ~ /^\.sync[\.-]/)
		return (1);
	else if (file ~ /^\./  &&  includedots == 0)
		return (1);
	else if (file ~ / /  &&  allowblanks == 0)
		return (1);

	return (0);
	}

function getpeerinfo(ftpd, file,   line, data) {

	if ((line = cfputc(ftpd, "SIZE", file, -213))+0 != 213)
		return ("");
			
	sub(/^[^ \t]+[ \t]+/, "", line);
	data = line;

	if ((line = cfputc(ftpd, "MDTM", file, -213))+0 != 213)
		return ("");
		
	sub(/^[^ \t]+[ \t]+/, "", line);
	sub(/\..*$/, "", line);
	data = data " " line;

	return (data);
	}

function readserverinfo(ftpd, dir, list,   file, line, data, dirlen) {
	delete list;

	#
	# Retrieve the list of all files and directories ...
	#

	portcmd = doport(ftpd, "");

	cfputc(ftpd, "NLST", ".", 150);
	portcmd |& getline line;

	dirlen = 0;
	if (dir != "") {
		dir = dir "/";
		dirlen = length(dir);
		}

	while (portcmd |& getline file) {
		file = noctrl(file);
		if (dirlen > 0  &&  substr(file, 1, dirlen) == dir)
			file = substr(file, dirlen + 1);

		if (excludefile(file))
			continue;

		list[file] = "";
		}

	close (portcmd);
	cfputc(ftpd, "", "", 226);


	#
	# ... and collect SIZE and MDTM for each file.
	#

	for (file in list) {
		if ((line = cfputc(ftpd, "SIZE", file, -213))+0 != 213) {
			delete list[file];
			continue;
			}
		else {
			sub(/^[^ \t]+[ \t]+/, "", line);
			data = line;
			}

		if ((line = cfputc(ftpd, "MDTM", file, -213))+0 != 213) {
			delete list[file];
			continue;
			}
		else {
			sub(/^[^ \t]+[ \t]+/, "", line);
			data = data " " line;
			}

		list[file] = data;
		}

	return (0);
	}


function doconnect(ftpserver, username, password,   server) {
	server = sprintf ("connect -x %s", ftpserver);
	interface = cfgets(server);

	sub(/:.*$/, "", interface);
	if (debug != 0)
		printf ("local interface is %s\n", interface);

	cfgets(server);				# peer information

	cfputc(server, "", "", 220);		# server greeting
	cfputc(server, "USER", username, 331);
	cfputc(server, "PASS", password, 230);

	return (server);
	}


function getnodeinfo(filename) {
	if (stat(filename, sbuf) != 0)
		return ("");

	return (sbuf["size"] " " strftime("%Y%m%d%H%M%S", sbuf["mtime"]));
	}


function readsyncinfo(filename, list,   line, x) {
	while (getline line <filename > 0) {
		split(line, x, /\t+/);
		list[x[1]] = x[2];
		}

	close(filename);
	return (0);
	}

function writesyncinfo(filename, list,   n, line, x) {
	for (file in list)
		printf ("%s\t%s\n", file, list[file]) >filename;

	close(filename);
	return (0);
	}

function computestatus(status, prev, current,   file) {
	for (file in prev) {
		if (! (file in current))
			status[file] = "d";
		else if (prev[file] == current[file])
			status[file] = "u";
		else
			status[file] = "c";
		}

	for (file in current) {
		if (! (file in prev))
			status[file] = "c";
		}

	return (0);
	}


function readconfig(filename, pw, sm,   lineno) {
	lineno = 0;
	while (getline <filename > 0) {
		lineno++;

		sub(/[ \t]*#.*$/, "", $0);
		if (NF == 0)
			continue;

		if ($1 == "name"  ||  $1 == "node"  ||  $1 == "nodename")
			nodename = $2;
		else if ($1 == "remote"  ||  $1 == "peer"  ||  $1 == "peername")
			peername = $2;
		else if ($1 == "server")
			ftpserver = $2;
		else if ($1 == "login")
			username = $2;
		else if ($1 == "password") {
			if (pw == "")
				password = $2;
			}
		else if ($1 == "syncmode"  ||  $1 == "mode") {
			if (sm == "")
				syncmode = $2;
			}
		else if ($1 == "symsync"  ||  $1 == "symmetric")
			symsync = 1;
		else if ($1 == "dir")
			rootdir = $2;
		else if ($1 == "dotfiles") {
			if (tolower($2) == "yes")
				includedots = 1;
			}
		else if ($1 == "allowblanks") {
			if (tolower($2) == "yes")
				allowblanks = 1;
			}
		else {
			printf ("%s: unknown config key: %s [%s:%s]\n",
				program, $1, filename, lineno);
			exit (1);
			}
		}

	close (filename);

	if (nodename == ""  ||  peername == "") {
		printf ("%s: unset node and/or peername: %s\n",
			program, filename);
		exit (1);
		}

	if (ftpserver !~ /:/)
		ftpserver = ftpserver ":21";

	return (0);
	}


function nextarg(par,   arg) {
	if (argi >= ARGC) {
		printf ("%s: missing argument: %s\n", program, par) >STDERR;
		exit (1);
		}
		
	arg = ARGV[argi];
	ARGV[argi++] = "";

	return (arg);
	}

BEGIN {
	extension("/usr/local/lib/awk.file.so", "dlload");
	STDERR = "/dev/stderr";

	synctable["sync"]     = "uu:nothing uc:get ud:remove ux:put" \
				" cu:put cc:duplicate cd:put cx:put" \
				" du:remove dc:get dd:ignore dx:ignore" \
				" xu:get xc:get xd:ignore xx:ignore";

	synctable["master"]   = "uu:nothing uc:get ud:put ux:put" \
				" cu:put cc:put cd:put cx:put" \
				" du:remove dc:get dd:ignore dx:ignore" \
				" xu:get xc:get xd:ignore xx:ignore";

	synctable["slave"]    = "uu:nothing uc:get ud:remove ux:put" \
				" cu:put cc:get cd:put cx:put" \
				" du:get dc:get dd:ignore dx:ignore" \
				" xu:get xc:get xd:ignore xx:ignore";

	synctable["mirror"]   = "uu:nothing uc:get ud:remove ux:ignore" \
				" cu:get cc:get cd:remove cx:ignore" \
				" du:get dc:get dd:ignore dx:ignore" \
				" xu:get xc:get xd:ignore xx:ignore";

	synctable["original"] = "uu:nothing uc:put ud:put ux:put" \
				" cu:put cc:put cd:put cx:put" \
				" du:remove dc:remove dd:ignore dx:ignore" \
				" xu:ignore xc:ignore xd:ignore xx:ignore";

	program = "ftpsync";
	argi = 1;
	debug = 0;
	conflictchar = ",";
	includedots = allowblanks = symsync = modeconfirm = 0;

	argi = 1;
	while (argi < ARGC  &&  substr(ARGV[argi], 1, 1) == "-") {
		options = nextarg("option");
		if (options == "--")
			break;

		for (i = 2; i<=length(options); i++) {
			c = substr(options, i, 1);
			if (c == "a")
				verbose = 1
			else if (c == "d")
				debug = 1;
			else if (c == "l")
				listonly = 1;
			else if (c == "p")
				password = nextarg("password");
			else if (c == "s") {
				modeset = 1;
				syncmode = nextarg("sync mode");
				if (! (syncmode in synctable)) {
					printf ("%s: unknown sync mode: %s\n", program, syncmode);
					exit (1);
					}
				}
			else if (c == "y")
				modeconfirm = 1;
			else if (c == "z")
				symsync = 1;
			else {
				printf ("%s: unkown option: -%s\n", program, c) >STDERR;
				exit (1);
				}
			}
		}

	dir = nextarg("local directory");
	if (chdir(dir) != 0) {
		printf ("%s: can't change to dir: %s, error= %s\n",
			program, dir, ERRNO) >STDERR;
		exit (1);
		}

	config = ".sync.conf";
	if (argi < ARGC)
		config = ".sync." nextarg("configuration file");

	readconfig(config, password, syncmode);
	nodesyncdata = sprintf (".sync-%s:%s", nodename, peername);
	peersyncdata = sprintf (".sync-%s:%s", peername, nodename);


	#
	# Set sync mode.
	#

	if (syncmode == "")
		syncmode = "sync";

	if (! (syncmode in synctable)) {
		printf ("%s: unknown sync mode: %s\n", program, syncmode);
		exit (1);
		}

	n = split(synctable[syncmode], x, " ");
	for (i=1; i<=n; i++) {
		split(x[i], y, ":");
		actiontab[y[1]] = y[2];
		}

	if (modeset != 0  &&  modeconfirm == 0) {
		split("u c d x", x, " ");
		for (i=1; i<=4; i++) {
			s = x[i];

			for (j=1; j<=4; j++)
				printf ("%c%s %-10s", s, x[j], actiontab[s x[j]]);

			printf ("\n");
			}

		listonly = 1;
		printf ("\n");
		}


	#
	# Read the server information first.  I also prefer to fail at
	# the beginning if the server has a problem (or is not properly
	# configured).
	#

	server = doconnect(ftpserver, username, password);
	if (rootdir != "")
		docwd(server, rootdir);

	readserverinfo(server, ".", currentpeer);
	readsyncinfo(peersyncdata, prevpeer);
	computestatus(peerstatus, prevpeer, currentpeer);




	#
	# Read current directory contents.
	#

	dirlist[""] = sbuf[""] = "";
	n = scandir(".", dirlist);
	for (i=1; i<=n; i++) {
		if (stat(dirlist[i], sbuf) != 0)
			dirlist[i] = "";
		else if (sbuf["type"] != "file")
			continue;

		if (excludefile(dirlist[i]))
			continue;

		currentnode[dirlist[i]] = getnodeinfo(dirlist[i]);
		}

	readsyncinfo(nodesyncdata, prevnode);
	computestatus(nodestatus, prevnode, currentnode);


	#
	# We compute the `does not exist' status for files we do not
	# have on both ends.
	#

	for (file in nodestatus) {
		if (! (file in peerstatus))
			peerstatus[file] = "x";
		}

	for (file in peerstatus) {
		if (! (file in nodestatus))
			nodestatus[file] = "x";
		}


	#
	# Now we can compute the file's action for this sync run.
	#

	for (file in nodestatus) {
		status[file] = s = nodestatus[file] peerstatus[file];
		action[file] = actiontab[s];

		if (listonly == 1)
			printf ("%s %s %s\n", s, action[file], file);
		}

	if (listonly == 1)
		exit (0);


	#
	# Let's synchronize.
	#

	for (file in action) {
		s = action[file];
		info = sprintf ("%s%c %s", status[file], toupper(substr(s, 1, 1)), file);

		if (s != "nothing"  ||  verbose == 1)
			printf ("%s\n", info);

		if (s == "nothing"  ||  s == "ignore")
			;
		else if (s == "get") {
			doretr(server, "I", file, file);
			currentnode[file] = getnodeinfo(file);
			}
		else if (s == "put") {
			dostor(server, "I", file, file);
			currentpeer[file] = getpeerinfo(server, file);
			}
		else if (s == "duplicate") {
			dup = file conflictchar peername;
			doretr(server, "I", file, dup);
			currentnode[file] = getnodeinfo(file);

			cmd = sprintf ("cmp -s '%s' '%s'", file, dup);
			if (system(cmd) == 0) {
				printf ("= %s\n", file);
				unlink(dup);
				currentpeer[file] = getpeerinfo(server, file);
				}
			else {
				dup = file conflictchar nodename;
				dostor(server, "I", file, dup);
				currentpeer[file] = getpeerinfo(server, file);
				}
			}
		else if (s == "remove") {
			if (file in currentnode) {
				unlink(file);
				delete currentnode[file];
				}

			if (file in currentpeer) {
				cfputc(server, "DELE", file, -250);
				delete currentpeer[file];
				}
			}
		else {
			printf ("%s: undefined action: %s, file= %s\n",
				program, s, file);
			exit (1);
			}
		}

	writesyncinfo(nodesyncdata, currentnode);
	writesyncinfo(peersyncdata, currentpeer);

	if (symsync != 0) {
		dostor(server, "I", nodesyncdata, peersyncdata);
		dostor(server, "I", peersyncdata, nodesyncdata);
		}

	cfputc(server, "QUIT", "", 221);
	exit (0);
	}

