#!/usr/bin/perl -w
# NetCP (C) Erkki Seppälä 1999-2007
use File::stat;
use IO::Handle;
use Time::HiRes;

STDERR->autoflush(1);

local $version = "0.2";
local $recursive = 0;
local $move = 0;

local $prevEstimate = 0;

while (@ARGV > 0 && $ARGV[0] =~ /^-/) {
  if ($ARGV[0] eq "-r") {
    $recursive = 1;
    shift @ARGV;
  }
  if ($ARGV[0] eq "-m") {
    $move = 1;
    shift @ARGV;
  }
  if ($ARGV[0] eq "-h") {
    shift @ARGV;
    print "netcp $version (c) Erkki Seppälä 1999-2007\n";
    print "cp with time estimation and continuing an interrupted copy (purely based on file sizes)";
    print "usage: netcp [-r] [-m] file1 [file2 [file3..]] target_dir\n";
    print "-r  recursive\n";
    print "-m  delete after copy\n";
    print "Note: cannot handle symbolic links.\n";
    exit;
  }
}

my @files = @ARGV;
my $target = pop @files;

$| = 1;

if (! scalar(@files)) {
  print STDERR "No files to copy.\n";
  exit 1;
}

my $targetIsDir = 0;
if ( -d $target ) {
  $targetIsDir = 1;
}

if ( !defined $target || ( ! $targetIsDir && @files > 1) ) { 
  print STDERR  "Target must be directory.\n";
  exit 1;
}

local @estimations = ();

print STDERR "Reading size..\r";
my $totalSize = 0;
for (my $c = 0; $c < @files; $c++) {
  $totalSize += du($files[$c]);
}

$totalSize = 1 if $totalSize == 0;

my $offset = 0;
for (my $c = 0; $c < @files; $c++) {
  if ( ! -d $files[$c]) {
    #	print "Would copy $files[$c] to $target/$baseFiles[$c]\n"; 
    if ($targetIsDir) {
      copyFile($files[$c], "$target/" . baseName($files[$c]), \$offset, $totalSize);
    } else {
      copyFile($files[$c], $target, \$offset, $totalSize);
    }
  } elsif ($recursive) {
    copyDirectory($files[$c], "$target/" . baseName($files[$c]), \$offset, $totalSize);
  } else {
    print STDERR "Ignoring directory ", baseName($files[$c]), "\n";
  }
}

sub feedEstimation {
  my ($pos) = @_;
  my $t = time;
  if (@estimations == 0 || $t - $estimations[@estimations - 1]->{'t'} >= 5) {
    push @estimations, {t=>$t, b=>$pos};
    if (@estimations > 60) {
      shift @estimations;
    }
  }
}

sub getRate {
  if (@estimations >= 2) {
    my $samples = 0;
    my $value = 0;
    my $balance = 1;
    for (my $i = 0; $i < 60 && $i < @estimations - 1; $i++) {
      my $diffTime = 
	$estimations[@estimations - 1 - $i]->{'t'} -
	  $estimations[@estimations - 2 - $i]->{'t'};
      my $diffBytes = 
	$estimations[@estimations - 1 - $i]->{'b'} -
	  $estimations[@estimations - 2 - $i]->{'b'};
      if ($diffTime) {
	$balance = 1/($i+1);
	$value += scalar($diffBytes / $diffTime) * $balance;
	$samples += $balance;
      }
    }
    return $samples ? ($value / $samples) : undef;
  } else {
    return undef;
  }
}

sub getEstimation {
  my ($size) = @_;
  my $rate = getRate();
  if ($rate) {
    my $time = ($size - $offset) / $rate;
    return $time;
  } else {
    return undef;
  }
}

sub dirName {
  my $f = $_[0];
  my $i = rindex($f, "/");
  if ($i != -1) {
    $f = substr($f, 0, $i);
  }
  return $f;
}

print "           \r";

sub baseName {
  my $f = $_[0];
  my $i = rindex($f, "/");
  if ($i != -1) {
    $f = substr($f, $i + 1, length($f) - 1);
  }
  return $f;
}

sub copyDirectory {
  my ($dirIn, $dirOut, $offsetRef, $totalSize) = @_;
  eval {
    my $dirInfo = stat($dirIn);
    # permissions will be set later, so we can be sure we can write there now
    mkdir("$dirOut", 0700);
    local *DIR;
    opendir(DIR, $dirIn);
    my @files = grep(!/^\.|\.\.$/, readdir(DIR));
    closedir(DIR);
    for (my $c = 0; $c < @files; $c++) {
      if (-d "$dirIn/$files[$c]") {
	if (copyDirectory("$dirIn/$files[$c]", "$dirOut/$files[$c]", $offsetRef, $totalSize)) {
	}
      } else {
	if (copyFile("$dirIn/$files[$c]", "$dirOut/$files[$c]", $offsetRef, $totalSize)) {
	}
      }
    }
    chmod $dirInfo->mode, $dirIn;
  };
  if ($@) {
    print "Problem copying directory $dirIn: $@\n";
    return 0;
  } else {
    if ($move) {
      rmdir "$dirIn";
    }
    return 1;
  }
}

sub copyFile {
  my ($fileIn, $fileOut, $offsetRef, $totalSize) = @_;
  # permissions will be set at the end, so continuing will work
  eval {
    local *IN;
    local *OUT;
    my $fileInfo = lstat $fileIn;
#x    if ($EUID == 0 && $fileInfo->
    open(IN, "<$fileIn");
    if ( ! -e $fileOut ) {
      open(OUT, ">$fileOut") or die "Cannot open $fileOut";
    } else {
      open(OUT, ">>$fileOut") or die "Cannot open $fileOut";
      my $st = stat($fileOut);
      seek(IN, $st->size, 0);
      $$offsetRef += $st->size;
    }
    my $written = 0;
    my $n = 0;
    print STDERR "copying ", baseName($fileIn), "\e[K\n";
    my $prevTime = Time::HiRes::time();
    my $waitRead = 0;
    my $waitWrite = 0;
    while ($bytes = read(IN, $buffer, int(1024 * 1024 / 10))) {
      feedEstimation($$offsetRef);
      my $now = Time::HiRes::time();
      if ($now > $prevEstimate + 1) {
        $prevEstimate = $now;
        my $rate = getRate();
        my $est = getEstimation($totalSize);
        my $bound = $waitRead > $waitWrite ? "read-bound" : "write-bound";
        print(int(100 * $$offsetRef / $totalSize),
  	    "%=", int($$offsetRef/1024), "kb/", int($totalSize / 1024), "kb",
  	    (defined $rate?(", ", int(getRate() / 1000), " kb/sec"):""), 
	    (defined $est?(", ", 
			   int($est / 60 / 60), " h ", 
			   int($est / 60 % 60), " m ",
			   int($est % 60), " s"
			  ):""), 
	    " $bound\e[K\r");
      }
      print OUT $buffer or die "Failed to write";
      my $now2 = Time::HiRes::time();
      $$offsetRef += $bytes;
      
      $waitRead += $now - $prevTime;
      $waitWrite += $now2 - $now;
      $prevTime = $now2;
    }
    print "\e[1Acopied $fileIn to $fileOut\e[K\n\e[K";
    close(OUT);
    close(IN);
    chmod $fileInfo->mode, $fileOut;
    utime $fileInfo->atime, $fileInfo->mtime, $fileOut;
  };
  if ($@) {
    print "Problem copying file $fileIn: $@\n";
    return 0;
  } else {
    if ($move) {
      unlink "$fileIn";
    }
    return 1;
  }
}

sub du {
  my $sum = 0;
  for (my $argc = 0; $argc < @_; $argc++) {
    if (-f $_[$argc]) {
      my $st = lstat("$_[$argc]");
      $sum += $st->size;
    } else {
      local *DIR;
      opendir(DIR, $_[0]);
      my @files = readdir(DIR);
      closedir(DIR);
      
      foreach my $k (@files) {
	if (my $st = lstat("$_[$argc]/$k")) {
	  if ($k ne "." and $k ne "..") {
	    if ($st->mode & 040000) {
	      if ($recursive) {
		$sum += du("$_[$argc]/$k") + $st->size;
	      }
	    } else {
	      $sum += $st->size;
	    }
	  }
	}
      }
    }
  }
  return $sum;
}
