]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/WwwStatic.pm
wwwstatic: implement Last-Modified and If-Modified-Since
[public-inbox.git] / lib / PublicInbox / WwwStatic.pm
1 # Copyright (C) 2016-2019 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3
4 package PublicInbox::WwwStatic;
5 use strict;
6 use Fcntl qw(:seek);
7 use HTTP::Date qw(time2str);
8
9 sub prepare_range {
10         my ($env, $in, $h, $beg, $end, $size) = @_;
11         my $code = 200;
12         my $len = $size;
13         if ($beg eq '') {
14                 if ($end ne '') { # "bytes=-$end" => last N bytes
15                         $beg = $size - $end;
16                         $beg = 0 if $beg < 0;
17                         $end = $size - 1;
18                         $code = 206;
19                 } else {
20                         $code = 416;
21                 }
22         } else {
23                 if ($beg > $size) {
24                         $code = 416;
25                 } elsif ($end eq '' || $end >= $size) {
26                         $end = $size - 1;
27                         $code = 206;
28                 } elsif ($end < $size) {
29                         $code = 206;
30                 } else {
31                         $code = 416;
32                 }
33         }
34         if ($code == 206) {
35                 $len = $end - $beg + 1;
36                 if ($len <= 0) {
37                         $code = 416;
38                 } else {
39                         sysseek($in, $beg, SEEK_SET) or return [ 500, [], [] ];
40                         push @$h, qw(Accept-Ranges bytes Content-Range);
41                         push @$h, "bytes $beg-$end/$size";
42
43                         # FIXME: Plack::Middleware::Deflater bug?
44                         $env->{'psgix.no-compress'} = 1;
45                 }
46         }
47         ($code, $len);
48 }
49
50 sub response {
51         my ($env, $h, $path, $type) = @_;
52         return unless -f $path && -r _; # just in case it's a FIFO :P
53
54         open my $in, '<', $path or return;
55         my $size = -s $in;
56         my $mtime = time2str((stat(_))[9]);
57
58         if (my $ims = $env->{HTTP_IF_MODIFIED_SINCE}) {
59                 return [ 304, [], [] ] if $mtime eq $ims;
60         }
61
62         my $len = $size;
63         my $code = 200;
64         push @$h, 'Content-Type', $type;
65         if (($env->{HTTP_RANGE} || '') =~ /\bbytes=([0-9]*)-([0-9]*)\z/) {
66                 ($code, $len) = prepare_range($env, $in, $h, $1, $2, $size);
67                 if ($code == 416) {
68                         push @$h, 'Content-Range', "bytes */$size";
69                         return [ 416, $h, [] ];
70                 }
71         }
72         push @$h, 'Content-Length', $len, 'Last-Modified', $mtime;
73         my $body = bless {
74                 initial_rd => 65536,
75                 len => $len,
76                 in => $in,
77                 path => $path,
78                 env => $env,
79         }, __PACKAGE__;
80         [ $code, $h, $body ];
81 }
82
83 # called by PSGI servers:
84 sub getline {
85         my ($self) = @_;
86         my $len = $self->{len} or return; # undef, tells server we're done
87         my $n = delete($self->{initial_rd}) // 8192;
88         $n = $len if $len < $n;
89         my $r = sysread($self->{in}, my $buf, $n);
90         if (defined $r && $r > 0) { # success!
91                 $self->{len} = $len - $r;
92                 return $buf;
93         }
94         my $m = defined $r ? "EOF with $len bytes left" : "read error: $!";
95         my $env = $self->{env};
96         $env->{'psgi.errors'}->print("$self->{path} $m\n");
97
98         # drop the client on error
99         if (my $io = $env->{'psgix.io'}) {
100                 $io->close; # this is likely PublicInbox::DS::close
101         } else { # for some PSGI servers w/o psgix.io
102                 die "dropping client socket\n";
103         }
104         undef;
105 }
106
107 sub close {} # noop, just let everything go out-of-scope
108
109 1;