The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Net::SSH::Any::SCP::Putter;

use strict;
use warnings;

use Carp;
use Fcntl ();

use Net::SSH::Any::Constants qw(SSHA_SCP_ERROR SSHA_REMOTE_CMD_ERROR);
use Net::SSH::Any::Util qw($debug _debug _debugf _debug_dump _debug_hexdump
                           _first_defined _inc_numbered _gen_wanted
                           _scp_escape_name _scp_unescape_name);

require Net::SSH::Any::SCP::Base;
our @ISA = qw(Net::SSH::Any::SCP::Base);

sub _new {
    my ($class, $any, $opts, $target) = @_;
    my $p = $class->SUPER::_new($any, $opts);
    $p->{target} = $target;
    $p->{recursive} = delete $opts->{recursive};
    $p->{send_time} = delete $opts->{send_time};
    $p;
}

sub read_dir {}
sub _read_dir {
    my ($p, $action) = @_;
    $p->read_dir($action ? ($action, $action->{_handle}): ());
}

sub open_dir {}
sub open_file {}
sub _open {
    my ($p, $action) = @_;
    my $method = "open_$action->{type}";
    my $handle = $p->$method($action);
    if (defined $handle) {
        $action->{_handle} = $handle;
        return 1;
    }
    else {
        $p->set_local_error($action, "unable to open directory or file for $action->{path}");
        return
    }
}

sub close_dir {}
sub close_file {}
sub _close {
    my ($p, $action) = @_;
    my $method = "close_$action->{type}";
    $p->$method($action, delete $action->{_handle}) and return 1;
    $p->set_local_error($action, "unable to close directory or file $action->{path}");
    return
}

sub _read_file {
    my ($p, $action, $len) = @_;
    $debug and $debug & 4096 and _debug_dump "_read_file action", $action;
    $p->read_file($action, $action->{_handle}, $len);
}

sub _send_line_and_get_response {
    my ($p, $dpipe, $action, $line) = @_;
    $debug and $debug & 4096 and
        _debug_hexdump("writting line", $line);
    my ($fatal, $error) = ( $dpipe->print($line)
                            ? $p->_read_response($dpipe)
                            : (2, "broken dpipe"));
    if ($fatal) {
        $p->set_remote_error($action, $error);
        $fatal > 1 and $p->abort;
        return;
    }
    return 1;
}

sub _remote_open {
    my ($p, $dpipe, $action) = @_;
    my ($type, $perm, $size, $name) = @{$action}{qw(type perm size name)};
    my $cmd = ($type eq 'dir'  ? 'D' :
               $type eq 'file' ? 'C' :
               croak "bad action type $action->{type}");
    $perm = (defined $perm ? $perm : 0777) & 0777;
    $debug and $debug & 4096 and
        _debugf("remote_open type: %s, perm: 0%o, size: %d, name: %s", $type, $perm, $size, $name);
    _scp_escape_name($name);
    $p->_send_line_and_get_response($dpipe, $action, sprintf("%s%04o %d %s\x0A", $cmd, $perm, $size, $name));
}

sub _clean_actions {
    my $p = shift;
    while (my $action = $p->_pop_action(undef, 1)) {
        $p->_close($action, 2, "broken dpipe");
    }
}

sub do_stat { 1 }

sub _do_stat {
    my ($p, $action) = @_;
    unless ($p->do_stat($action)) {
        $p->set_local_error($action, "unable to retrieve file system properties for $action->{path}");
        return;
    }
    unless (defined $action->{type}) {
        $action->{type} = (Fcntl::S_ISDIR($action->{perm} || 0) ? 'dir' : 'file');
    }
    1;
}

sub _link_check {
    my ($p, $action) = @_;
    if (not $p->{follow_links} and Fcntl::S_ISLNK($action->{perm} || 0)) {
        $p->set_local_error($action, "not a regular file");
        return;
    }
    1;
}

sub _dir_check {
    my ($p, $action) = @_;
    if (not $p->{recursive} and $action->{type} eq 'dir') {
        $p->set_local_err
    }
}

sub on_end_of_put { 1 }

sub _send_time {
    my ($p, $dpipe, $action) = @_;
    return 1 unless $p->{send_time};
    my ($mtime, $atime) = @{$action}{'mtime', 'atime'};
    $p->_send_line_and_get_response($dpipe, $action,
                                    sprintf("T%d %d %d %d\x0A", $mtime, 0, $atime, 0));
}

sub _send_file {
    my ($p, $dpipe, $action) = @_;
    my $failed = 0;
    my $remaining = $action->{size} || 0;
    while ($remaining > 0) {
        my $data;
        my $len = ($remaining > 16384 ? 16386 : $remaining);
        if ($failed) {
            $data = "\0" x $len;
        }
        else {
            $data = $p->_read_file($action, $len);
            unless (defined $data and length $data) {
                $failed = 1;
                $debug and $debug & 4096 and _debug "no data from putter";
                redo;
            }
            if (length($data) > $remaining) {
                $debug and $debug & 4096 and _debug("too much data, discarding excess");
                substr($data, $remaining) = '';
                $failed = 1;
            }
        }
        $debug and $debug & 4096 and _debug_hexdump("sending data (failed: $failed)", $data);
        $dpipe->print($data) or last OUT;
        $remaining -= length $data;
    }
    $p->_close($action) or $failed = 1;
    $p->_send_line_and_get_response($dpipe, $action, ($failed ? "\x01failed\x0A" : "\x00"));
}

sub run {
    my ($p, $opts) = @_;
    my $any = $p->{any};
    my $dpipe = $any->dpipe({ %$opts, quote_args => 1 },
                            # 'strace', '-fo', '/tmp/scp.strace',
                            $p->{scp_cmd},
                            '-t',
                            ($p->{send_time}   ? '-p' : ()),
                            ($p->{recursive}   ? '-r' : ()),
                            ($p->{double_dash} ? '--' : ()),
                            $p->{target} );
    $any->error and return;

    local $SIG{PIPE} = 'IGNORE';

    my ($error_level, $error_msg) = $p->_read_response($dpipe);
    if ($error_level) {
	$any->_or_set_error(SSHA_SCP_ERROR, "remote SCP refused transfer", $error_msg);
	return;
    }

 OUT: while (not $p->{aborted}) {
        my $line;
        my $current_dir_action = $p->{actions}[-1];
        if (my $action = $p->_read_dir($current_dir_action)) {
            my $type = $action->{type};
            $action = $p->_push_action(%$action);

            $debug and $debug & 4096 and _debug_dump("next action", $action);

            # local_error actions are just pushed into the log
            unless (defined $type and $type eq 'local_error') {
                if ($p->_do_stat($action)) {
                    $type = $action->{type};
                    if ($type eq 'dir' and not $p->{recursive}) {
                        $debug and $debug & 4096 and _debug "discarding directory $action->{path}";
                        $p->set_local_error($action, "not a regular file");
                    }
                    else {
                        if ($p->_open($action)) {
                            if ($p->_send_time($dpipe, $action)) {
                                if ($p->_remote_open($dpipe, $action)) {
                                    if ($type eq 'dir') {
                                        # do not pop the action from the actions stack;
                                        next;
                                    }
                                    elsif ($type eq 'file') {
                                        $p->_send_file($dpipe, $action);
                                    }
                                }
                            }
                            else {
                                $p->_close($action);
                            }
                        }
                    }
                }
            }
            $p->_pop_action;
        }
        else { # close dir
            my $action = $p->_pop_action('dir', 1) or last;
            $p->_close($action);
            $p->_send_line_and_get_response($dpipe, $action, "E\x0A");
        }
    }

    $dpipe->close;

    $p->_clean_actions;

    $p->on_end_of_put or
        $p->_or_set_error(SSHA_SCP_ERROR, "SCP transfer not completely successful");

    not $any->error
}

1;