René Nyffenegger's collection of things on the web
René Nyffenegger on Oracle - Most wanted - Feedback -
 

ftp diff [PERL script]

ftp_diff.pl
use strict;
use warnings;
use NET::FTP;

my $host          = $ARGV[0];
my $user          = $ARGV[1];
my $pass          = $ARGV[2];

my $ftp_dir_top   = $ARGV[3];
my $local_dir_top = $ARGV[4];

my $ftp = Net::FTP -> new ($host) or die 'could not establish ftp connection';
$ftp -> login($user, $pass) or die 'login failed';

my @cur_path = ();
diff_dir(\@cur_path);

sub diff_dir {
  my $cur_path_ref = shift;

  my $path_relative = '/' . (join '/', map {$_->{dir}} @{$cur_path_ref}) . '/';

  my $local_dir = $local_dir_top.$path_relative;
  my $ftp_dir   =   $ftp_dir_top.$path_relative;

  die "not a dir $local_dir" unless -d $local_dir;

  opendir DIR, $local_dir or die "could not open $local_dir";
  my @dir_list_local_ = sort readdir DIR;
  closedir DIR;
  my @dir_list_local;
  for my $local_dir_or_file (@dir_list_local_) {
    if (-d "$local_dir/$local_dir_or_file") {
      push @dir_list_local, {name => $local_dir_or_file, type=>'d'};
    }
    elsif (-f "$local_dir/$local_dir_or_file") {
      push @dir_list_local, {name => $local_dir_or_file, type=>'f'};
    }
    else {
      die "$local_dir_or_file seems to be neither file nor directory";
    }
  }


  $ftp -> cwd ($ftp_dir_top . $path_relative) or die "ftp: could not cd to $ftp_dir_top - $path_relative";
  $ftp -> cwd ($ftp_dir) or die "ftp: could not cd to $ftp_dir";
  my @dir_list_ftp_   = $ftp -> dir;

  my @dir_list_ftp;
  for my $ftp_dir_or_file (@dir_list_ftp_) {
    # Assumption: $ftp_dir_or_file looks like
    #   drwxr-xr-x   2 user group     4096 Sep 28 21:15 dir_d_sub_1
    # or
    #   -rw-r--r--   1 user group       18 Sep 28 21:51 one.txt
    my $name = (split ' ', $ftp_dir_or_file)[8];
    my $type = substr($ftp_dir_or_file, 0, 1);

    if ($type eq 'd') {
      push @dir_list_ftp, {name => $name, type=>'d'};
    }
    elsif ($type eq '-') {
      push @dir_list_ftp, {name => $name, type=>'f'};
    }
    else {
      die "$ftp_dir_or_file neither 'd' nor '-'";
    }
  }

  my $cur_file_local;
  my $cur_file_ftp;

  $cur_file_local = next_local_file_or_dir(\@dir_list_local);
  $cur_file_ftp   = next_ftp_file_or_dir  (\@dir_list_ftp  );

  while (defined $cur_file_ftp or defined $cur_file_local) {
    if (defined $cur_file_ftp and defined $cur_file_local) {

      if ($cur_file_ftp->{name} eq $cur_file_local->{name}) {

        if ($cur_file_ftp->{type} eq 'd' and $cur_file_local->{type} eq 'd') {
           push @{$cur_path_ref}, {dir=>$cur_file_ftp->{name}, already_printed=>0};
           diff_dir($cur_path_ref);
           pop  @{$cur_path_ref};
        }
        elsif ($cur_file_ftp->{type} ne $cur_file_local->{type}) {
           print_path_and_text("$cur_file_ftp->{name}: ftp has type $cur_file_ftp->{type}, local has type $cur_file_local->{type}", $cur_path_ref);
        }

        $cur_file_local = next_local_file_or_dir(\@dir_list_local);
        $cur_file_ftp   = next_ftp_file_or_dir  (\@dir_list_ftp  );

      }
      elsif ($cur_file_ftp->{name} lt $cur_file_local->{name}) {
        missing($cur_file_ftp  , $cur_path_ref, 'local', \$cur_file_ftp, \$cur_file_local, \@dir_list_ftp, \@dir_list_local);
      }
      else { # $cur_file_ftp gt $cur_file_local
        missing($cur_file_local, $cur_path_ref, 'ftp', \$cur_file_ftp, \$cur_file_local, \@dir_list_ftp, \@dir_list_local);
      }
    }
    elsif (defined $cur_file_ftp and not defined $cur_file_local) {
      missing($cur_file_ftp  , $cur_path_ref, 'local', \$cur_file_ftp, \$cur_file_local, \@dir_list_ftp, \@dir_list_local);
    }
    elsif (not defined $cur_file_ftp and defined $cur_file_local) {
      missing($cur_file_local, $cur_path_ref, 'ftp', \$cur_file_ftp, \$cur_file_local, \@dir_list_ftp, \@dir_list_local);
    }
    else {
      die "should not have happened...";
    }
  }
}

sub print_path_and_text {

  my $text         = shift;
  my $cur_path_ref = shift;

  my $indent=-1;
  for my $path_elem (@$cur_path_ref) {
    $indent ++;
    next if $path_elem->{already_printed};

    print (("  " x $indent). $path_elem->{dir} . "   [$indent]\n");
    $path_elem->{already_printed} = 1;
  }

  print (("  " x ($indent+1)). "$text\n");

}

sub missing {
  my $file           = shift;
  my $cur_path_ref   = shift;
  my $where          = shift;
  my $ftp_file_ref   = shift;
  my $local_file_ref = shift;
  my $ftp_list_ref   = shift;
  my $local_list_ref = shift;

  print_path_and_text("$file->{name} missing $where", $cur_path_ref);

  if ($where eq 'ftp') {
   $$local_file_ref = next_local_file_or_dir($local_list_ref);
  }
  else {
    $$ftp_file_ref = next_ftp_file_or_dir($ftp_list_ref);
  }
}

sub next_local_file_or_dir {
  my $local_list_ref = shift;

  my $ret;

  while (1) {
    if (@$local_list_ref) {
      my $file_or_dir_name_ = shift @$local_list_ref;
      next if $file_or_dir_name_ -> {name} eq '.' or $file_or_dir_name_ -> {name} eq '..';
      $ret = $file_or_dir_name_;

      last;
    }
    else {
      undef $ret;
      last;
    }
  }

  return $ret;
}

sub next_ftp_file_or_dir {
  my $ftp_list_ref = shift;

  my $ret;

  while (1) {
    if (@$ftp_list_ref) {
      my $file_or_dir_name_ = shift @$ftp_list_ref;
      next if $file_or_dir_name_ -> {name} eq '.' or $file_or_dir_name_ -> {name} eq '..';

      $ret = $file_or_dir_name_;
      last;

    }
    else {
      undef $ret;
      last;
    }
  }

  return $ret;
}
The script is called with something like:
perl ftp_diff.pl ftp.server.tld username password /htdocs c:/dev/htdocs
where /htdocs specifies the top directory on the ftp server and c:/dev/htdocs the local top directory.

Testcase

ftp_diff_testcase.pl
use warnings;
use strict;
use NET::FTP;
use File::Path;
use File::Copy;

my $host          = $ARGV[0];
my $user          = $ARGV[1];
my $pass          = $ARGV[2];

my $ftp_dir_top   = $ARGV[3];
my $local_dir_top = $ARGV[4];


## logging into ftp server...
my $ftp = Net::FTP -> new ($host) or die 'could not establish ftp connection';
$ftp -> login($user, $pass) or die 'login failed';

## Clean up previous run...

$ftp -> rmdir ($ftp_dir_top, 1);
rmtree($local_dir_top);

##

$ftp -> mkdir ($ftp_dir_top);
mkpath      ($local_dir_top);

$ftp -> mkdir ($ftp_dir_top . "/dir_a");
mkpath      ($local_dir_top . "/dir_a");

$ftp -> mkdir ($ftp_dir_top . "/dir_b");
$ftp -> mkdir ($ftp_dir_top . "/dir_b/dir_b_sub");
#mkpath      ($local_dir_top . "dir_b");

#$ftp -> mkdir ($ftp_dir_top . "dir_c");
mkpath      ($local_dir_top . "/dir_c");
mkpath      ($local_dir_top . "/dir_c/dir_c_sub");

$ftp -> mkdir ($ftp_dir_top . "/dir_d");
mkpath      ($local_dir_top . "/dir_d");

$ftp -> mkdir ($ftp_dir_top . "/dir_d/dir_d_sub_1");
mkpath      ($local_dir_top . "/dir_d/dir_d_sub_1");

$ftp -> put ('x',   $ftp_dir_top . "/dir_d/dir_d_sub_1/one.txt");
copy        ('x', $local_dir_top . "/dir_d/dir_d_sub_1/one.txt");

$ftp -> mkdir ($ftp_dir_top . "/dir_d/dir_d_sub_2");
mkpath      ($local_dir_top . "/dir_d/dir_d_sub_2");

$ftp -> put ('x',   $ftp_dir_top . "/dir_d/dir_d_sub_2/aaa.txt");
copy        ('x', $local_dir_top . "/dir_d/dir_d_sub_2/bbb.txt");

$ftp -> put ('x',   $ftp_dir_top . "/dir_d/dir_d_sub_2/ccc.txt");
copy        ('x', $local_dir_top . "/dir_d/dir_d_sub_2/ccc.txt");

$ftp -> put ('x',   $ftp_dir_top . "/dir_d/dir_d_sub_2/eee.txt");
copy        ('x', $local_dir_top . "/dir_d/dir_d_sub_2/ddd.txt");

$ftp -> mkdir ($ftp_dir_top . "/dir_e/");
mkpath      ($local_dir_top . "/dir_e/");


$ftp -> mkdir (   $ftp_dir_top   . "/dir_e/a_dir_on_ftp_a_file_local");
copy        ('x', $local_dir_top . "/dir_e/a_dir_on_ftp_a_file_local");

$ftp -> put ('x',   $ftp_dir_top . "/dir_e/a_file_on_ftp_a_dir_local");
mkpath      (     $local_dir_top . "/dir_e/a_file_on_ftp_a_dir_local");

system ("perl ftp_diff.pl.raw $host $user $pass $ftp_dir_top $local_dir_top");