The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl

package Tie::FTP;

use strict;
use warnings;

use File::Temp ();

our $VERSION = 0.01;

sub TIEHANDLE { # uri object || uri || netftp object, path
	my $pkg = shift || return undef;
	
	my ($tmpfh,$tmpnm) = File::Temp::tmpnam();
	
	my $self = bless { tmpfile => $tmpnm, tmpfh => $tmpfh},$pkg;
	
	$self;
}

sub OPEN {
	my $self = shift;
	
	if (scalar @_ > 1){
		$self->ftp(shift);
		$self->path(shift);
	} else { # uri or uri object
		my $uri = shift;
		$uri = URI->new($uri) unless ref $uri;
		return undef unless $uri->scheme eq 'ftp';
		
		$self->ftp(Net::FTP->new($uri->host));
		$self->ftp->login(split(':',$uri->userinfo));
		$self->path(substr($uri->path,1));
	}
	
	$self->ftp->get($self->path,$self->tmpfile);
}

sub tmpfile { # set to use a tempfile instead of writing and reading via net. overrides cache
	my $self = shift;
	$self->{tmpfile} = shift if @_;
	$self->{tmpfile};
}

sub tmpfh {
	my $self = shift;
	$self->{tmpfh} = shift if @_;
	$self->{tmpfh};
}

sub ftp {
	my $self = shift;
	$self->{ftp} = shift if @_;
	$self->{ftp};
}

sub path {
	my $self = shift;
	$self->{path} = shift if @_;
	$self->{path};
}

sub taint {
	my $self = shift;
	$self->{tainted} = 1;
}

sub tainted {
	my $self = shift;
	$self->{tainted};
}

sub CLOSE {
	goto &UNTIE;
}

sub UNTIE { }

sub DESTROY {
	my $self = shift;
	close $self->tmpfh;
	$self->ftp->put($self->tmpfile,$self->path) if $self->tainted;
	unlink $self->tmpfile;
}


sub WRITE { $_[0]->taint; my $fh = $_[0]{tmpfh}; print $fh substr($_[1],$_[3],$_[2]) }
sub PRINT { $_[0]->taint; my $fh = shift->{tmpfh}; print $fh @_ }
sub PRINTF { $_[0]->taint; my $fh = shift->{tmpfh}; printf $fh @_ }
sub READ { read *{$_[0]{tmpfh}},$_[1],$_[2],$_[3] }
sub READLINE { readline $_[0]{tmpfh} }
sub GETC { getc *{$_[0]{tmpfh}} }
sub BINMODE{ binmode *{$_[0]{tmpfh}} }
sub EOF { eof *{$_[0]{tmpfh}} }
sub TELL { tell *{$_[0]{tmpfh}} }
sub SEEK { seek *{$_[0]{tmpfh}},$_[1],$_[2] }

1; # Keep your mother happy.

__END__

=pod

=head1 NAME

Tie::FTP - A module to open files on FTP servers as filehandles

=head1 SYNOPSIS

	tie *FH,'Tie::FTP';

	open FH,'ftp://user:password@host/file';

	print while (<FH>);

	seek FH,0,0;
	print FH foo;
	close FH;

Or

	tie *FH,'Tie::FTP';
	
	(tied *FH)->taint;
	my $fh = (tied *FH)->tmpfh;
	
	seek $fh,0,0;
	print $fh foo;
	close FH;

=head1 DESCRIPTION

This module downloads a file on an FTP server into a temporary file, and allows editing on that. Upon destroy the object rewrites itself to the server if there were any write operations.

=head1 AVOIDING THE TIE INTERFACE

Since all the operations are actually delegated you're better off tying a gensym, and then using C<my $fh = (tied $ftpfh)-E<gt>tmpfh;>. Just remember to call C<(tied $ftpfh)-E<gt>taint;> if you want to commit changes at DESTROY time.

=head1 METHODS

=over 4

=item TIEHANDLE

This method accepts either a URI string or object, or a logged in Net::FTP object and a path string.

In the former all login credentials are sucked out of the URI, and a Net::FTP object is created. See CAVEATS.

In the latter form the Net::FTP object is assumed to be connected, and the path string is a relative one.

=back

=head1 CAVEATS

The file will be written regardless of wether or not there are write permissions.

You may need to do C<(tied $ftpfh)-E<gt>ftp-E<gt>noop;> every once in a while, to stir things up.

Net::FTP and URI are not used from within the module as you may prefer to pass other types of objects, with a compatible interface.

=head1 AUTHOR

Yuval Kogman <nothingmuch@woobling.org>

=head1 COPYRIGHT

peh.

=cut