/usr/local/CPAN/MogileFS-Client/MogileFS/ClientHTTPFile.pm


package MogileFS::ClientHTTPFile;

use strict;

use LWP::UserAgent;
use HTTP::Request;
use HTTP::Status;
use Errno qw(EIO EINVAL EPERM);

use fields ('mg',
            'fid',
            'devid',
            'class',
            'key',
            'path',
            'length',
            'pos',
            'ua',
            'eof',
            'readonly',
            'readLineChunkSize',
            );


sub TIEHANDLE {
    my MogileFS::ClientHTTPFile $self = shift;

    $self = fields::new($self) unless ref $self;

    my %args = @_;

    $self->{devid} = $args{devid};
    $self->{path}  = $args{path};
    $self->{readLineChunkSize}  = $args{readLineChunkSize} || 4096;

    $args{backup_dests} ||= [];

    my $ua  = LWP::UserAgent->new( keep_alive => 60, timeout => 5 );

    while ($self->{path}) {
        my $req;
        # overwrite needs changing to create if not exists?
        if ($args{overwrite}) {
            $req = HTTP::Request->new( PUT => $self->{path} ); # Ensure file overwritten/created, even if they don't print anything
        } else {
            $req = HTTP::Request->new( HEAD => $self->{path} );
        }

        my $res = $ua->request( $req );

        if ($res->is_success) {
            if ($args{overwrite}) {
                $self->{length} = 0;
            } else {
                $self->{length} = $res->header( 'Content-Length' ) || 0;
            }

            last;
        } else {
            my $dest = shift @{$args{backup_dests}};

            if ($dest) {
                $self->{devid} = $dest->[0];
                $self->{path}  = $dest->[1];
            } else {
                $self->{devid} = undef;
                $self->{path}  = undef;
            }
        }
    }

    return unless $self->{path};

    $self->{pos}      = 0;
    $self->{ua}       = $ua;
    $self->{eof}      = 0;

    $self->{mg}       = $args{mg};
    $self->{fid}      = $args{fid};
    $self->{key}      = $args{key};
    $self->{readonly} = $args{readonly} || 0;

    return $self;
}
*new = *TIEHANDLE;

sub READ {
    my MogileFS::ClientHTTPFile $self = shift;
    my $buf = \$_[0]; shift;
    my ($len, $offset) = @_;

    defined( $$buf ) or $$buf = '';
    defined( $offset ) or $offset = 0;

    if ($len == 0) {
        $$buf = '';
        return 0;
    }

    die "Negative len [$len] passed" if $len < 0;

    die "Negative offset [$offset] not supported" if $offset < 0;

    return 0 if ($self->EOF);

    my $start = $self->{pos};
    my $end   = $self->{pos} + $len - 1;

    my $req = HTTP::Request->new(GET => $self->{path}, [
        Range => "bytes=$start-$end",
    ], );

    my $res = $self->{ua}->request( $req );

    if ($res->is_error) {
        if ($res->code eq RC_REQUEST_RANGE_NOT_SATISFIABLE) {
            $self->{eof} = 1;
            return 0;
        }
        
        $! = EIO;
        return;
    }

    my $length = length( $res->content );

    $self->{pos} += $length;

    # Behaviour is not correct with offsets < length of existing buffer
    if ($offset) {
        $$buf = substr($$buf, 0, $offset) . $res->content;
    } else {
        $$buf = $res->content;
    }

    return $length;
}
*read = *READ;

sub WRITE {
    my MogileFS::ClientHTTPFile $self = shift;

    my ($buf, $len, $offset) = @_;

    if ($self->{readonly}) {
        $! = EPERM;
        return;
    }

    if (defined $len || defined $offset) {
        $offset = 0 if ! defined $offset;

        $buf = substr($buf, $offset, $len);
    }

    $len = length($buf);

    my $start = $self->{pos};
    my $end   = $self->{pos} + $len - 1;

    my $req = HTTP::Request->new(PUT => $self->{path}, [
        'Content-Range' => "bytes $start-$end/*",
    ], );

    $req->add_content($buf);

    my $res = $self->{ua}->request( $req );

    if ($res->is_error) {      
        $! = EIO;
        return;
    }

    if ($self->{pos} + $len > $self->{length}) {
        $self->{length} = $self->{pos} + $len;
    }

    $self->{pos} += $len;

    $self->{eof} = ($self->{pos} == $self->{length} ? 1 :0); 

    return $len;
}
*write = *WRITE;

sub EOF {
    my MogileFS::ClientHTTPFile $self = shift;

    return 1 if $self->{eof};

    return unless $self->{length};

    return $self->{pos} >= $self->{length};
}
*eof = *EOF;

sub TELL {
    my MogileFS::ClientHTTPFile $self = shift;

    return $self->{pos};
}
*tell = *TELL;

sub SEEK {
    my MogileFS::ClientHTTPFile $self = shift;

    my ($offset, $whence) = @_;

    if ($whence == 1) {
        $offset += $self->{pos};
    } elsif ($whence == 2) {
        $offset += $self->{length};
    }

    if ($offset > $self->{length}) {
        $! = EINVAL;
        return 0;
    }

    $self->{pos} = $offset;
    $self->{eof} = ($self->{pos} == $self->{length} ? 1 :0);

    return 1;
}
*seek = *SEEK;

sub GETC {
    my MogileFS::ClientHTTPFile $self = shift;

    $self->READ( my $buf, 1 );
   
    return $buf;
}
*getc = *GETC;

sub PRINT {
    my MogileFS::ClientHTTPFile $self = shift;

    my $buf = join(defined $, ? $, : "", @_);

    $buf .= $\ if defined $\;

    $self->WRITE($buf, length($buf), 0);
}
*print = *PRINT;

sub PRINTF {
    my MogileFS::ClientHTTPFile $self = shift;
    
    my $buf = sprintf(shift,@_);

    $self->WRITE($buf,length($buf),0);
}
*printf = *PRINTF;

sub CLOSE {
    my MogileFS::ClientHTTPFile $self = shift;

    if ($self->{devid}) {
       my $mg = $self->{mg};

        my $rv = $mg->{backend}->do_request
            ("create_close", {
                fid    => $self->{fid},
                devid  => $self->{devid},
                domain => $mg->{domain},
                size   => $self->{length},
                key    => $self->{key},
                path   => $self->{path},
            });
        
        unless ($rv) {
            $@ = "$mg->{backend}->{lasterr}: $mg->{backend}->{lasterrstr}";
            return undef;
        }
    }

    return 1;
}
*close = *CLOSE;

sub BINMODE {
    return 1;
}
*binmode = *BINMODE;

sub FILENO {
    # Wanted by perl debugger
    return -1;
}
*fileno = *FILENO;

# Must return undef (not just '') on EOF
sub READLINE {
    my MogileFS::ClientHTTPFile $self = shift;

    my $retBuff;
    my $startPos = $self->{pos};
    my $foundEol;
READ:
    while (!$self->EOF) {
        my $readBuff;
        my $rc = $self->read($readBuff, $self->{readLineChunkSize});
        # Undef $/ => we will only exit on EOF (which should be right)
        $foundEol = index($readBuff, $/) if defined $/;
        if (defined($foundEol) && $foundEol >= 0) {
            $foundEol += length($/);
            $retBuff ||= '';
            $retBuff .= substr($readBuff, 0, $foundEol);
            # We have over-read, so go back
            $self->seek($startPos + length($retBuff) , 0);
            last READ;
        }
        else {
            # Go round again
            $retBuff .= $readBuff;
        }
    }
    return $retBuff;
}
*readline = *READLINE;

sub path {
    my MogileFS::ClientHTTPFile $self = shift;

    return $self->{path};
}

1;