#!/usr/bin/perl
# ------------------ ftpsync.pl ---------------------------------------------
# A tool for syncing files between the local filesystem and an FTP server, or
# vice versa.
#
# Copyright (c) 2003/2004 Matthias Kramm <matthias.kramm@mgm-edv.de>
# 
# Usage: see usage().
# ---------------------------------------------------------------------------

use Net::FTP;

$SOURCE_HOST = "";
$SOURCE_DIR = "";
$SOURCE_USERNAME = "";
$SOURCE_PASSWORD = "";
$TARGET_HOST = "";
$TARGET_DIR = "";
$TARGET_USERNAME = "";
$TARGET_PASSWORD = "";
$DEBUG = 0;
$PASSIVE = 0;
$MODIFY = 1;
$DELETE = 1;

# ------------------ parse options -------------------------------------------

%LONG2SHORT = ('passive' => 'p', 'debug' => 'd', 'dry-run' => 's', 'help' => 'h');

sub usage {
    print "\n";
    print "Usage: ftpsync.pl user/password\@host:<path> <path>\n";
    print "   OR: ftpsync.pl <path> user/password\@host:<path>\n";
    print "\n";
    print "Options:\n";
    print "    -d , --debug         Switch on generation of debug messages\n";
    print "    -s , --dry-run       Do not actually change any files, just show what would happen\n";
    print "    -p , --passive       Set FTP mode to passive\n";
    print "    -k , --keep          Don't delete files on target server\n";
    print "    -h , --help          This info.\n";
    print "\n";
}

foreach (@ARGV) {
    if(/^-/) {
	if(/^--/) {
	    $option = $LONG2SHORT{substr($_,2)};
	} else {
	    $option = substr($_,1);
	}
	if($option eq 'd') {
	    $DEBUG = 1;
	} elsif($option eq 'p') {
	    $PASSIVE = 1;
	} elsif($option eq 'k') {
	    $DELETE = 0;
	} elsif($option eq 's') {
	    $MODIFY = 0;
	} elsif($option eq 'h') {
	    usage();
	    exit;
	}
    }
    elsif(!$SOURCE_DIR_SET) {
	if(/^([^\/]*)(\/([^\/]*))?\@(.*):(.*)$/) {
	    $SOURCE_HOST = $4;
	    $SOURCE_DIR = $5;
	    $SOURCE_USERNAME = $1;
	    $SOURCE_PASSWORD = $3;
	} else {
	    $SOURCE_HOST = "";
	    $SOURCE_USERNAME= "";
	    $SOURCE_PASSWORD= "";
	    $SOURCE_DIR = $_;
	}
	$SOURCE_DIR_SET=1;
    }
    elsif(!$TARGET_DIR_SET) {
	if(/^([^\/]*)(\/([^\/]*))?\@(.*):(.*)$/) {
	    $TARGET_HOST = $4;
	    $TARGET_DIR = $5;
	    $TARGET_USERNAME = $1;
	    $TARGET_PASSWORD = $3;
	} else {
	    $TARGET_HOST = "";
	    $TARGET_USERNAME= "";
	    $TARGET_PASSWORD= "";
	    $TARGET_DIR = $_;
	}
	$TARGET_DIR_SET=1;
    }
    else {
	print stderr "Unknown option: $_\n";
    }
}

if(!$SOURCE_DIR_SET)
{usage();exit;}

die "Both source and destination must be supplied." if(!$TARGET_DIR_SET);

print "Transferring from $SOURCE_USERNAME/$SOURCE_PASSWORD\@$SOURCE_HOST:$SOURCE_DIR\n";
print "Transferring to   $TARGET_USERNAME/$TARGET_PASSWORD\@$TARGET_HOST:$TARGET_DIR\n";
print "[debug]" if($DEBUG);
print "[passive]" if($PASSIVE);
print "[dry run]" if(!$MODIFY);
print "\n\n";

# ------------------ open connection -------------------------------------------

die "Source and destination can't both be FTP connections" if($SOURCE_HOST && $TARGET_HOST);
die "One of source or destination must be a FTP connection" if(!$SOURCE_HOST && !$TARGET_HOST);

$level = 0;
if($SOURCE_HOST) {
    ($user,$password,$host,$dir) = ($SOURCE_USERNAME,$SOURCE_PASSWORD,$SOURCE_HOST,$SOURCE_DIR);
} else {
    ($user,$password,$host,$dir) = ($TARGET_USERNAME,$TARGET_PASSWORD,$TARGET_HOST,$TARGET_DIR);
}
$ftp = Net::FTP->new($host, Debug => $DEBUG,  Passive => $PASSIVE) or die "new failed";
$ftp->login($user, $password) or die "login failed";
$ftp->binary() or die "binary failed";
$ftp->cwd("$dir") or die "remote cwd to $dir failed";
if($SOURCE_HOST) {
    chdir($TARGET_DIR) or die "local chdir to $TARGET_DIR failed";
    $ftp2local=1;
} else {
    chdir($SOURCE_DIR) or die "local chdir to $SOURCE_DIR failed";
    $local2ftp=1;
}
syncfiles();
$ftp->quit() or print "quit failed";

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

sub syncfiles
{
    #my @remote_files = $ftp->dir() or die "could not get FTP directory contents";
    my @remote_files = $ftp->dir();

    my $prefix = " "x($level*4);

    my %local_files;
    while(<*>) {
	$local_files{$_}=$_;
    }

    foreach (@remote_files) {
	my ($permission,$links,$user,$group,$size,$month,$day,$time,$filename)=split /[ \t\n\r]+/;
	
	delete $local_files{$filename};
	    
	if($permission =~ /^d/) { #directory
	    next if($filename =~ /^tmp$/);
	    my $mkdir = 0;
	    if($ftp2local && !-d $filename) {
		print "[+d] $prefix$filename/\n";
		mkdir($filename) or die "Couldn't mkdir $filename";
		$mkdir = 1;
	    }
	    if(-d $filename) {
		if(!$mkdir) {
		    print "[CD] $prefix$filename/\n";
		}
		chdir($filename) or die "could not chdir locally to $filename";
		$ftp->cwd($filename) or die "could not chdir remotely to $filename";
		$level++;
		syncfiles();
		$level--;
		chdir("..") or die "could not chdir locally to .. from $CWD";
		$ftp->cwd("..") or die "could not chdir remotely to ..";
	    } else {
		print "[--] $prefix$filename/\n";
		if($MODIFY) {
		    if($DELETE) {
			die;
			$ftp->rmdir($filename) or ftpremove($filename);
		    }
		}
	    }
	    if(!$MODIFY && $mkdir) {
		rmdir($filename);
	    }
	} else { #file
	    next if($filename =~ /^sync$/);
	    if(-f $filename) {
		my ($d,$i,$m, $n,$uid,$gid, $r,$size, $at, $mt,$ct,$bsize,$b) = stat($filename);
		my $remote_size = $ftp->size($filename);

		if($size==$remote_size) {
		    print "[  ] $prefix$filename\n";
		} else {
		    if($ftp2local) {
			print "[DL] $prefix$filename\n";
			if($MODIFY) {
			    $ftp->get($filename, $filename) or die "Couldn't get file $filename";
			}
		    } else {
			print "[UP] $prefix$filename\n";
			if($MODIFY) {
			    $ftp->put($filename, $filename)  or die "Couldn't put file $filename";
			}
		    }
		}
	    } else {
		if($ftp2local) {
		    print "[DL] $prefix$filename\n";
		    if($MODIFY) {
			$ftp->get($filename, $filename) or die "Could not get file $filename";
		    }
		} else {
		    print "[--] $prefix$filename\n";
		    if($MODIFY) {
			if($DELETE) {
			    die;
			    $ftp->delete($filename) or die "couldn't remove $filename\n";
			}
		    }
		}
	    }
	}
    }
    foreach $filename (keys %local_files) {
	if(-f $filename) {
	    next if($filename =~ /^sync$/);
	    if($ftp2local) {
		print "[--] $prefix$filename\n";
		if($MODIFY) {
		    unlink($filename) or die "Couldn't unlink $filename\n";
		}
	    } else {
		print "[++] $prefix$filename\n";
		if($MODIFY) {
		    $ftp->put($filename,$filename) or die "Couldn't put $filename\n";
		}
	    }
	} else {
	    next if($filename =~ /^tmp$/);
	    if($ftp2local) {
		print "[-d] $prefix$filename/\n";
		if($MODIFY) {
		    rmdir($filename) or localremove($filename)
		}
	    } else {
		print "[+d] $prefix$filename/\n";
		$ftp->mkdir($filename) or die "couldn't mkdir $filename";
		chdir($filename) or die "could not chdir locally to $filename";
		$ftp->cwd($filename) or die "could not chdir remotely to $filename";
		$level++;
		syncfiles();
		$level--;
		chdir("..") or die "could not chdir locally to .. from $CWD";
		$ftp->cwd("..") or die "could not chdir remotely to ..";
	    }
	}
    }
}

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

# recursively remove a directory over FTP.
sub ftpremove
{
    if(!$DELETE) {
	return;
    }
    die;
    my $dir = shift;

    $ftp->cwd($dir) or die "could not chdir remotely to $dir";
    my @remote_files = $ftp->dir;
    foreach (@remote_files) {
	my ($permission,$links,$user,$group,$size,$month,$day,$time,$filename)=split /[ \t\n\r]+/;
	if($permission =~ /^d/) {
	    ftpremove($filename);
	} else {
	    $ftp->delete($filename);
	}
    }
    $ftp->cwd("..") or die "could not chdir remotely to ..";
		
    $ftp->rmdir($dir) or die "could not rmdir $dir";
}

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

# recursively remove a directory locally.
sub localremove
{
    my $dir = shift;

    chdir($dir) or die "could not chdir locally to $dir";

    my @files = <*>;
    foreach (@files) {
	if(-d $_) {
	    localremove($_);
	} else {
	    unlink($_) or die "could not unlink local file $_";
	}
    }
    chdir("..") or die "could not chdir to .. (?)";
    rmdir($dir) or die "could not remove directory $dir";
}
