# Implementation of the file protocol for LWP::Parallel, based on # LWP::Protocol::file and LWP::Parallel::Protocol::ftp pattern. # contributed by Jeff Behr, October 2001 # $Id: file.pm,v 1.2 2003/05/26 08:03:34 langhein Exp $ package LWP::Parallel::Protocol::file; use HTTP::Status (); use HTTP::Response (); use LWP::MediaTypes (); use IO::File(); use IO::Dir(); use vars qw(@ISA); require LWP::Parallel::Protocol; require LWP::Protocol::file; @ISA = qw(LWP::Parallel::Protocol LWP::Protocol::file); use strict; # this method just sees that the file or directory exists and can # be read by the user, etc., and then creates a handle for it from # IO::File or IO::Dir sub handle_connect { my ($self, $request, $proxy, $timeout, $nonblock) = @_; LWP::Debug::trace('(Entered Parallel::Protocol::file::handle_connect)'); # check proxy if (defined $proxy) { my $res = HTTP::Response->new(&HTTP::Status::RC_BAD_REQUEST, 'You cannot proxy through the filesystem'); return(undef, $res); } #check method my $method = $request->method; unless ($method eq 'GET' || $method eq 'HEAD' || $method eq 'DELETE') { my $res = HTTP::Response->new(&HTTP::Status::RC_METHOD_NOT_ALLOWED, "Method $method not allowed for 'file:' URLs"); return(undef, $res); } # check url my $url = $request->url; my $scheme = $url->scheme; if ($scheme ne 'file') { my $res = HTTP::Response->new(&HTTP::Status::RC_INTERNAL_SERVER_ERROR, "LWP::file::handle_connect called for '$scheme'"); return(undef, $res); } ######## # URL OK ######## # If we get here, URL is OK my $path = $url->file; # test file exists and is readable unless (-e $path) { my $res = HTTP::Response->new(&HTTP::Status::RC_NOT_FOUND, "File '$path' does not exist."); return(undef, $res); } unless (-r _) { my $res = HTTP::Response->new(&HTTP::Status::RC_FORBIDDEN, "User does not have read permission"); return(undef, $res); } if ($method eq 'DELETE' && !(-w _)) { my $res = HTTP::Response->new(&HTTP::Status::RC_FORBIDDEN, "User does not have permission to delete $path"); return(undef, $res); } # file exists and is readable/writable ... my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$filesize, $atime,$mtime,$ctime,$blksize,$blocks) = stat(_); # check if-modified-since my $ims = $request->header('If-Modified-Since'); if (defined $ims) { my $time = HTTP::Date::str2time($ims); if (defined $time and $time >= $mtime) { my $res = HTTP::Response->new(&HTTP::Status::RC_NOT_MODIFIED, "$method $path"); return(undef, $res); } } # the return value is an object of IO::Handle, either # IO::File or IO::Dir. # Ooops. Turns out IO::Dir is not derived from IO::Handle and # IO::Select calls in UserAgent->wait calls don't see a handle. # for objects of IO::Dir even though they can be "connections". # Return (undef, response) for directory calls, for now. Prob- # ably have to one-time directory lists in the future, or skip # doing dirs here in favor of list_urls in FileCopy.pm. my $fh; if (-d _) { return (undef, HTTP::Response->new(&HTTP::Status::RC_INTERNAL_SERVER_ERROR, "Skipping directory handle for '$path'.")); #$fh = IO::Dir->new($path) or return (undef, # HTTP::Response->new(&HTTP::Status::RC_INTERNAL_SERVER_ERROR, # "Unable to create directory handle for '$path': $!")); } elsif (-f _) { $fh = IO::File->new($path) or return (undef, HTTP::Response->new(&HTTP::Status::RC_INTERNAL_SERVER_ERROR, "Unable to create file handle for '$path': $!")); } else { return (undef, HTTP::Response->new(&HTTP::Status::RC_UNSUPPORTED_MEDIA_TYPE, "'$path' is not a directory or file listing.")); } # Response looks to be OK my $response = HTTP::Response->new(&HTTP::Status::RC_OK, "OK"); $response->request($request); # Add header(s) $response->header('Last-Modified', HTTP::Date::time2str($mtime)); return ($fh, $response); } sub write_request { my ($self, $req, $fh, $response, $arg, $timeout) = @_; LWP::Debug::trace('(Entered Parallel::Protocol::file::write_request)'); # $fh should be an IO::File or IO::Dir unless (ref($fh) eq 'IO::File' or ref($fh) eq 'IO::Dir') { my $res = HTTP::Response->new(&HTTP::Status::RC_UNSUPPORTED_MEDIA_TYPE, "Socket is not IO::File or IO::Dir"); return(undef, $res); } # Delete the file, return the response. if ($req->method eq 'DELETE') { my $cnt = unlink $req->uri->file; my $res; if ($cnt) { $res = HTTP::Response->new(&HTTP::Status::RC_OK, "Deleted $req->uri->file"); } else { $res = HTTP::Response->new(&HTTP::Status::RC_METHOD_NOT_ALLOWED, "Deletion failed on $req->uri->file"); } return(undef, $res); } # return input $fh/$socket, response return($fh, $response); } sub read_chunk { my ($self, $response, $fh, $request, $arg, $size, $timeout, $entry) = @_; LWP::Debug::trace('(Entered Parallel::Protocol::file::read_chunk)'); $size = 32768 unless defined $size and $size > 0; my $path = $request->uri->path; my $method = $request->method; #print "Performing $method on $path\n"; # this is redundant from &handle_connect - see if it can be streamlined my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$filesize, $atime,$mtime,$ctime,$blksize,$blocks) = stat($path); # Collect the data from the dir or file. If it's a dir, we get it in # one shot and send it back to collect_once. Otherwise, we try to stay # with the Parallel fashion and do things through wait(). Doing it this # way will, I think, minimize the amount of memory that gets sucked up. my $buf = ""; if (ref($fh) eq 'IO::File') { # LWP::Proto::file does nothing with files under HEAD - # sets header(s) from the values returned by stat, etc. $response->header('Content-Length', $filesize); my $type = LWP::MediaTypes::guess_media_type($path, $response); my $bytes; ($buf, $bytes) = $self->_read_file($fh, $response, $size); # receive() method bases its action on the $arg value my $retval = $self->receive($arg, $response, \$buf, $entry); # $retval from Parallel::Proto->receive() # this should be bytes read or a constant error value # Could do more with the return value here return (defined $retval ? $retval : $bytes); } elsif (ref($fh) eq 'IO::Dir') { $buf = $self->_read_dir($fh, $response); if ($ENV{DIR_AS_HTML}) { ($buf, $response) = $self->_write_as_html($buf, $response); $response->header('Content-Type', 'text/html'); } else { $response->header('Content-Type', 'text/plain'); } $response->header('Content-Length', length $buf); $buf = "" if $method eq "HEAD"; $self->collect_once($arg, $response, $buf); return 0; } else { my $res = HTTP::Response->new(&HTTP::Status::RC_UNSUPPORTED_MEDIA_TYPE, "Socket is not IO::File or IO::Dir"); # Not too sure about this return value return 0; } } sub close_connection { my ($self, $response, $fh, $request, $socket) = @_; LWP::Debug::trace('(Entered Parallel::Protocol::file::close_connect)'); $fh->close; # Dir or File return; } sub _read_file { my ($self, $fh, $response, $size) = @_; my $content; #$fh->binmode; my $bytes_read = $fh->sysread($content, $size); $content, $bytes_read; } # when reading directories, just get it all in one shot sub _read_dir { my $self = shift; my $fh = shift; my $res = shift; my @files = sort $fh->read; # Make full directory listing my $path = $res->request->uri->path; for (@files) { if($^O eq "MacOS") { $_ .= "/" if -d "$path:$_"; } else { $_ .= "/" if -d "$path/$_"; } } my $files = join "", @files; return $files; } sub _write_as_html { my ($self, $filelist, $response) = @_; my $path = $response->request->uri->path; # Re-Make directory listing my @files = split '\n', $filelist; for (@files) { my $furl = URI::Escape::uri_escape($_); # file's url my $desc = HTML::Entities::encode($_); # file's link $_ = qq{