=head1 NAME

DRSync - Functions for drsync

=head1 SYNOPSIS

    use DRSync;

    # simple way
    drsync(@ARGV);

    # or a complex one
    foreach my $arglist (@arglists) {
        eval { drsync(@$arglist); }
        warn "Error: $@, arglist: ".join(" ",@$arglist)."\n" 
            if $@;
    }

=head1 DESCRIPTION

This module does file synchronization, as described in the
L<drsync|drsync>
page. 

You can use the I<drsync> function to do the work. Arguments for drsync
are the command line parameters for the I<drsync>. You better call
the I<drsync> function in an I<eval> block, because it can throw
exceptions.

=head1 SEE ALSO

L<drsync|drsync>

=cut

package DRSync;
use strict;
use Exporter;
use vars qw(@ISA @EXPORT $VERSION $REVISION $BASENAME);
use Getopt::Mixed;
use File::Basename;
use File::Copy qw(move);
use IO::Handle;

@ISA = qw(Exporter);
@EXPORT = qw(drsync ispawn ospawn);

$VERSION = '0.4.3';
$REVISION = q$Id: DRSync.pm 93 2004-06-17 19:42:36Z dlux $;
$BASENAME = basename($0);

sub drsync_jobs {
    local @ARGV = @_;
}

sub drsync {
    local @ARGV = @_;
    my @COPY_ARGV = @ARGV;

    Getopt::Mixed::init( 
        "rsync=s rsh=s bzip2=s gzip=s state_file=s state-file>state_file ".
        "n dry-run>n v:i verbose>v q quiet>q u update>u existing progress ".
        "b backup>b suffix:s version" 
    );

    my (@rsync_opts, @optional_args);
    $Getopt::Mixed::badOption = sub { my ($pos,$option,$reason) = @_;
        push @rsync_opts, $option;
        return ("","","");
    };

    local $ENV{LANG} = "C"; # Make sure that rsync uses english messages

    # setting up default options

    my %opt;
    $opt{rsync} = "rsync";
    $opt{bzip2} = "bzip2";
    $opt{gzip} = "gzip";
    $opt{rsh} = $ENV{RSYNC_RSH} || "rsh";
    $opt{state_file} = undef;
    $opt{suffix} = '~';

    while (my ($option, $value, $pretty) = Getopt::Mixed::nextOption()) {
        next if !$option;
        $option =~ s/\W/_/g;
        $opt{$option} = defined $value ? $value : 1;
    }
    Getopt::Mixed::cleanup();

    $opt{v} ||= 0;
    push @rsync_opts,"--rsh=$opt{rsh}";
    push @rsync_opts,"--exclude","*$opt{suffix}" if $opt{b};
    push @optional_args,"-v" if $opt{v}>1;
    push @optional_args,"-q" if $opt{q};
    push @optional_args,"--progress" if $opt{progress};
    push @optional_args,"-n" if $opt{n};

    die "$BASENAME $VERSION ($REVISION)\n" if $opt{version};
    die "Usage: $BASENAME [options] src [...] dest\n".
        "'perldoc drsync' for more information\n\n" if @ARGV<2;

    my @srcdir = @ARGV;
    my ($dest, $desthost, $destdir) = pop(@srcdir) =~ /^((?:(.*):)?(.*))$/;

    # If "state-file" is not provided, we simply call rsync

    if (!defined $opt{state_file}) {
        system($opt{rsync}, @COPY_ARGV);
        return;
    }

    # recovering a broken session

    if (-f $opt{state_file}."~") {
        move $opt{state_file}."~", $opt{state_file}
            or die "Cannot recover state file from backup: $!";
    }

    # opening state file

    my $listfile_perms = 0666 ^ umask();
    my ($file, $old_filelist, $del_filelist, $add_filelist, $new_filelist);

    if (-f $opt{state_file}) {
        $listfile_perms = (stat($opt{state_file}))[2] & 07777;
        if ($opt{state_file} =~ /\.bz2$/i) {
            $file = ospawn($opt{bzip2}, "-cd", $opt{state_file}) or die $!;
        } elsif ($opt{state_file} =~ /\.gz$/i) {
            $file = ospawn($opt{gzip}, "-cd", $opt{state_file}) or die $!;
        } else {
            open $file, $opt{state_file} or die $!;
        }

        # reading the file

        while (<$file>) {
            chomp;
            $old_filelist->{$_}=1;
        }

        close $file;
    } else {
        $old_filelist={};
    }

    # generating filelist

    print "Getting file list: $opt{rsync} -n --stats @rsync_opts @srcdir /dev\n"
        if $opt{v};

    $file = ospawn($opt{rsync},"-n","--stats",@rsync_opts,@srcdir,"/dev");

    my $rsyncstate = 0; # start state
    while (<$file>) {
        chomp;

        # pre-filenames
        if ($rsyncstate == 0) {
            next if /^rsync/ || /^receiving file list/ || /^building file list/;
            $rsyncstate = 1;
        }

        next if /^skipping non-regular file/;

        # filename processing
        if ($rsyncstate == 1) {
            if (/^$/ || /^rsync\[.*heap statistics/) {
                $rsyncstate = 2 ; # last state, do nothing
            } else {
                $new_filelist->{$_}=1;
            }
        }
    }

    close $file;

    die "Filelist generation error, exiting\n" if $rsyncstate!=2;

    # Creating "add" and "del" filelist hash

    foreach my $key (keys %$new_filelist) {
        if (exists $old_filelist->{$key}) {
            delete $old_filelist->{$key};
        } else {
            $add_filelist->{$key} = 1;
        }
    }

    %$del_filelist = %$old_filelist;

    if ($desthost) {
        print "Making r/ssh connection: $opt{rsh} $desthost sh\n" 
            if $opt{v};
        $file=ispawn($opt{rsh},$desthost,"sh");
    } else {
        open $file,"|sh";
    }

    print $file "cd '$destdir'\n";

    # Removing files which are removed here

    foreach my $key (keys %$del_filelist) {
        $key =~ s/'/'\\''/g;
        print "-$key\n" if $opt{v};
        print $file "[ -f '$key' ] && ".
            ($opt{b} ? "mv -f '$key' '$key$opt{suffix}'" : "rm -f '$key'" ).
            "\n"
                if !$opt{n};
    };

    # Adding new files to the other side

    foreach my $key (keys %$add_filelist) {
        $key =~ s/'/'\\''/g; # shell escape
        print "+$key\n" if $opt{v};
        my $dir = dirname $key;
        if (!$opt{n}) {
            print $file "[ -d '$dir' ] || mkdir -p '$dir'\n" if $dir;
            print $file "[ -f '$key' ] || touch -t 197001011200 '$key'\n";
        }
    }

    close $file;

    if (!$opt{n}) {

        # Writing out the filelist for a temporary file

        my $tempname = $opt{state_file}.".new.$$";

        if ($opt{state_file} =~ /\.bz2$/i) {
            $file = ispawn("'$opt{bzip2}' >'$tempname'") or die $!;
        } elsif ($opt{state_file} =~ /\.gz$/i) {
            $file = ispawn("'$opt{gzip}' >'$tempname'") or die $!;
        } else {
            open $file, ">$tempname" or die $!;
        }

        print $file join("\n",keys %$new_filelist);

        close $file;

        chmod $listfile_perms, $tempname
            or warn "Cannot chmod state file: $!";

        # Copying temp file to replace the new one

        if (-f $opt{state_file}) {
            move $opt{state_file}, $opt{state_file}."~" 
                or die "Cannot make backup: $!"
        }

        move $tempname,$opt{state_file} 
            or die "Cannot move temp state file: $!";
        if (-f $opt{state_file}."~") {
            unlink $opt{state_file}."~" 
                or die "Cannot unlink temp state file: $!";
        }

    }

    # Calling the final "rsync" to do the rest of the work

    print 
        "Executing: $opt{rsync} --update --existing ".
        "@rsync_opts @optional_args @srcdir $dest\n";

    system($opt{rsync}, "--update", "--existing", 
        @rsync_opts, @optional_args, @srcdir, $dest) if !$opt{n};
}

# ospawn: forks a process, and returns the filehandle of the stdout of the
# process

sub ospawn { my (@args)=@_;
    open OFD, "-|" or exec @args;
    OFD->autoflush(1);
    return \*OFD;
}

# ispawn: forks a process, and returns the filehandle of the stdin of the
# process

sub ispawn { my (@args)=@_;
    open IFD, "|-" or exec @args;
    IFD->autoflush(1);
    return \*IFD;
}

1;
