/usr/local/CPAN/LWPng-alpha/LWP/Conn/FILE.pm


package LWP::Conn::FILE;
use strict;
require LWP::Version;

# Ideally, we should make this implementation shareable with
# HTTP::Daemon.

use HTTP::Date qw(time2str str2time);
use LWP::MediaTypes qw(guess_media_type);

# Test to see if the system has getpwuid and getgrgid.
eval { my $tmp = getpwuid($<); };
my $has_getpwuid = ! $@;
eval { my $tmp = getgrgid($(); };
my $has_getgrgid = ! $@;

sub new
{
    my($class, %cnf) = @_;
    my $mgr = delete $cnf{ManagedBy} ||
      Carp::croak("'ManagedBy' is mandatory");
    # don't care about other configuration parameters yet

    # process all request in the queue
    while (my $req = $mgr->get_request(__PACKAGE__)) {
	my $url = $req->url;
	my $host = $url->host;
	if ($host && $host ne "localhost") {
	    # generate redirect to ftp serveer
	    my $loc = $url->as_string;
	    $loc =~ s/^\w+:/ftp:/;
	    $req->give_response(301, "Use ftp instead", {Location => $loc});
	    next;
	}

	my $method = uc($req->method);
	my $path = $url->file;

	if ($method eq "HEAD" || $method eq "GET") {
	    get($req, $path, $method eq "GET");

	} elsif ($method eq "PUT") {
	    if ($req->header("Content-Range")) {
		$req->give_response(506, "Don't handle partial content updates yet");
		next;
	    }
	    put($req, $path);

	} elsif ($method eq "DELETE") {
	    # XXX must really handle If-XXX headers
	    if (unlink($path)) {
		$req->give_response(204, "OK");
	    } else {
		$req->give_response(errno_status(), "$!");
	    }

	} elsif ($method eq "TRACE") {  # Just for fun!
	    my $res = $req->new_response(200, "OK");
	    $res->date(time);
	    $res->server($LWP::Version::PRODUCT_TOKEN);
	    $res->content_type("message/http");
	    $res->content($req->as_string);
	    $req->response_done($res);

	} else {
	    $req->give_response(405, "Bad method '$method'");
	}
    }

    undef;  # not really a connection
}

sub get
{
    my($req, $path, $send_content) = @_;

    local(*DIR);
    if (opendir(DIR, $path)) {
	dir($req, $path, \*DIR, $send_content);
	closedir(DIR);
	return;
    }

    local(*FILE);
    if (sysopen(FILE, $path, 0)) {
	my $res = $req->new_response(200, "OK");
	my $now = time;

	$res->date($now);
	$res->server($LWP::Version::PRODUCT_TOKEN);

	my($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$filesize,
	   $atime,$mtime,$ctime,$blksize,$blocks) = stat(FILE);

	my $uname = ($has_getpwuid ? getpwuid($uid) : undef) || $uid;
	my $gname = ($has_getgrgid ? getgrgid($gid) : undef) || $gid;

	# far more than you ever wanted to know
	$res->header("INode" => sprintf("[%04x]:%d", $dev, $ino)) if $ino;
	$res->header("Owner" => $uname);
	$res->header("Group" => $gname);
	$res->header("Content-Length" => $filesize);
	$res->header("Blocks-Allocated" => $blocks);
	$res->header("Last-Modified" => time2str($mtime));
	$res->header("Last-Accessed" => time2str($atime));
	$res->header("Status-Modified" => time2str($ctime));

	$res->header("Content-Location" => "file:$path"); # XXX absolutize
	guess_media_type($path, $res);

	# We use the same algoritm as Apache to generate an etag.
	my $etag = sprintf qq("%x-%x-%x"), $ino, $filesize, $mtime;
	$etag = "W/$etag" if $now - $mtime < 2;
	$res->header("ETag" => $etag);

	# Check various If-XXX headers
	if (my $ius = $req->header("If-Unmodified-Since")) {
	    $ius = str2time($ius);
	    if ($ius && $mtime > $ius) {
		$res->code(412); # PRECONDITION_FAILED
		$res->message("Resouce modified");
		close(FILE);
		$req->response_done($res);
		return;
	    }
	}

	if (my @im = $req->header("If-Match")) {
	    my $im = join(", ", @im);
	    my $orig_im = $im;
	    if ($im ne "*") {
		my $match = 0;
		while (length($im)) {
		    if ($im =~ s|^\s*(W/)?(\"[^\"]*\")\s*,?\s*||) {
			next if $1;  # must use strong comparison
			if ($2 eq $etag) {
			    $match++;
			    last;
			}
		    } else {
			last;  # illegal value
		    }
		}
		#$res->header("X-Unprocessed-If-Match", $im) if $im;
		unless ($match) {
		    $res->code(412); # PRECONDITION_FAILED
		    $res->message("No match for ETag $orig_im");
		    close(FILE);
		    $req->response_done($res);
		    return;
		}
	    }
	}

	my $skip_if_modified;
	if (my @inm = $req->header("If-None-Match")) {
	    my $inm = join(", ", @inm);
	    my $match;
	    my $etag2 = $etag;
	    $etag2 =~ s,^W/,,;
	    $match = "*" if $inm eq "*";
	    while (!$match && length($inm)) {
		if ($inm =~ s|^\s*(W/?(\"[^\"]*\"))\s*,?\s*||) {
		    $match = $1 if $2 eq $etag;
		} else {
		    last;  # illegal value
		}
	    }
	    if ($match) {
		#$res->code(412); # PRECONDITION_FAILED
		$res->code(304); # NOT_MODIFIED
		$res->message("ETag match for $match");
		close(FILE);
		$req->response_done($res);
		return;
	    }
	    $skip_if_modified++;
	}
	
	if (!$skip_if_modified &&
	    (my $ims = $req->header("If-Modified-Since"))) {
	    $ims = str2time($ims);
	    if ($ims && $mtime <= $ims) {
		$res->code(304);
		$res->message("Not modified");
		close(FILE);
		$req->response_done($res);
		return;
	    }
	}

	# XXX Implement the Range header???

	if ($send_content) {
	    my $buf;
	    while (my $n = sysread(FILE, $buf, 1024)) {
		eval {
		    $req->response_data($buf, $res);
		};
		if ($@) {
		    chomp($@);
		    $res->header('X-Died' => $@);
		    last;
		}
	    }
	}
	close(FILE);
	$req->response_done($res)

    } else {
	$req->give_response(errno_status(), "$!");
    }
}

sub dir
{
    my($req, $path, $dir, $send_content) = @_;
    $req->give_response(501, "Directory reading", $path); #NYI
}

sub put
{
    my($req, $path) = @_;
    $req->give_response(501, "File updating", $path); #NYI
}

sub errno_status
{
    if ($! =~ /No such file/) {
	return 404;
    } else {
	return 403;
    }
}

1;