]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/WwwStatic.pm
No ext_urls
[public-inbox.git] / lib / PublicInbox / WwwStatic.pm
index e1f536f3f21c1d7ce63ea74f140d2646fb1284b1..1c1a3d38d6e7c5fd9a8c479b5dc013809b66405b 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2016-2019 all contributors <meta@public-inbox.org>
+# Copyright (C) 2016-2021 all contributors <meta@public-inbox.org>
 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
 
 # This package can either be a PSGI response body for a static file
@@ -9,14 +9,15 @@
 # functionality of nginx.
 package PublicInbox::WwwStatic;
 use strict;
+use v5.10.1;
 use parent qw(Exporter);
-use bytes ();
 use Fcntl qw(SEEK_SET O_RDONLY O_NONBLOCK);
 use POSIX qw(strftime);
 use HTTP::Date qw(time2str);
 use HTTP::Status qw(status_message);
 use Errno qw(EACCES ENOTDIR ENOENT);
 use URI::Escape qw(uri_escape_utf8);
+use PublicInbox::GzipFilter qw(gzf_maybe);
 use PublicInbox::Hval qw(ascii_html);
 use Plack::MIME;
 our @EXPORT_OK = qw(@NO_CACHE r path_info_raw);
@@ -51,7 +52,19 @@ sub r ($;$) {
          [ $msg ] ]
 }
 
-sub prepare_range {
+sub getline_response ($$$$$) {
+       my ($env, $in, $off, $len, $path) = @_;
+       my $r = bless {}, __PACKAGE__;
+       if ($env->{'pi-httpd.async'}) { # public-inbox-httpd-only mode
+               $env->{'psgix.no-compress'} = 1; # do not chunk response
+               %$r = ( bypass => [$in, $off, $len, $env->{'psgix.io'}] );
+       } else {
+               %$r = ( in => $in, off => $off, len => $len, path => $path );
+       }
+       $r;
+}
+
+sub setup_range {
        my ($env, $in, $h, $beg, $end, $size) = @_;
        my $code = 200;
        my $len = $size;
@@ -81,9 +94,6 @@ sub prepare_range {
                if ($len <= 0) {
                        $code = 416;
                } else {
-                       if ($in) {
-                               sysseek($in, $beg, SEEK_SET) or return r(500);
-                       }
                        push @$h, qw(Accept-Ranges bytes Content-Range);
                        push @$h, "bytes $beg-$end/$size";
 
@@ -95,7 +105,7 @@ sub prepare_range {
                push @$h, 'Content-Range', "bytes */$size";
                return [ 416, $h, [] ];
        }
-       ($code, $len);
+       ($code, $beg, $len);
 }
 
 # returns a PSGI arrayref response iff .gz and non-.gz mtimes match
@@ -108,7 +118,9 @@ sub try_gzip_static ($$$$) {
        return unless -f $gz && (stat(_))[9] == $mtime;
        my $res = response($env, $h, $gz, $type);
        return if ($res->[0] > 300 || $res->[0] < 200);
-       push @{$res->[1]}, qw(Cache-Control no-transform Content-Encoding gzip);
+       push @{$res->[1]}, qw(Cache-Control no-transform
+                               Content-Encoding gzip
+                               Vary Accept-Encoding);
        $res;
 }
 
@@ -142,30 +154,37 @@ sub response ($$$;$) {
        my $len = $size;
        my $code = 200;
        push @$h, 'Content-Type', $type;
+       my $off = 0;
        if (($env->{HTTP_RANGE} || '') =~ /\bbytes=([0-9]*)-([0-9]*)\z/) {
-               ($code, $len) = prepare_range($env, $in, $h, $1, $2, $size);
+               ($code, $off, $len) = setup_range($env, $in, $h, $1, $2, $size);
                return $code if ref($code);
        }
        push @$h, 'Content-Length', $len, 'Last-Modified', $mtime;
-       my $body = $in ? bless {
-               initial_rd => 65536,
-               len => $len,
-               in => $in,
-               path => $path,
-               env => $env,
-       }, __PACKAGE__ : [];
-       [ $code, $h, $body ];
+       [ $code, $h, $in ? getline_response($env, $in, $off, $len, $path) : [] ]
 }
 
 # called by PSGI servers on each response chunk:
 sub getline {
        my ($self) = @_;
+
+       # avoid buffering, by becoming the buffer! (public-inbox-httpd)
+       if (my $tmpio = delete $self->{bypass}) {
+               my $http = pop @$tmpio; # PublicInbox::HTTP
+               push @{$http->{wbuf}}, $tmpio; # [ $in, $off, $len ]
+               $http->flush_write;
+               return; # undef, EOF
+       }
+
+       # generic PSGI runs this:
        my $len = $self->{len} or return; # undef, tells server we're done
-       my $n = delete($self->{initial_rd}) // 8192;
+       my $n = 8192;
        $n = $len if $len < $n;
+       sysseek($self->{in}, $self->{off}, SEEK_SET) or
+                       die "sysseek ($self->{path}): $!";
        my $r = sysread($self->{in}, my $buf, $n);
        if (defined $r && $r > 0) { # success!
                $self->{len} = $len - $r;
+               $self->{off} += $r;
                return $buf;
        }
        my $m = defined $r ? "EOF with $len bytes left" : "read error: $!";
@@ -199,7 +218,7 @@ my %path_re_cache;
 sub path_info_raw ($) {
        my ($env) = @_;
        my $sn = $env->{SCRIPT_NAME};
-       my $re = $path_re_cache{$sn} ||= do {
+       my $re = $path_re_cache{$sn} //= do {
                $sn = '/'.$sn unless index($sn, '/') == 0;
                $sn =~ s!/\z!!;
                qr!\A(?:https?://[^/]+)?\Q$sn\E(/[^\?\#]+)!;
@@ -256,12 +275,11 @@ sub dir_response ($$$) {
        my $path_info = $env->{PATH_INFO};
        push @entries, '..' if $path_info ne '/';
        for my $base (@entries) {
+               my @st = stat($fs_path . $base) or next; # unlikely
                my $href = ascii_html(uri_escape_utf8($base));
                my $name = ascii_html($base);
-               my @st = stat($fs_path . $base) or next; # unlikely
-               my ($gzipped, $uncompressed, $hsize);
-               my $entry = '';
                my $mtime = $st[9];
+               my ($entry, $hsize);
                if (-d _) {
                        $href .= '/';
                        $name .= '/';
@@ -277,12 +295,12 @@ sub dir_response ($$$) {
                        next;
                }
                # 54 = 80 - (SP length(strftime(%Y-%m-%d %k:%M)) SP human_size)
-               $hsize = sprintf('% 8s', $hsize);
                my $pad = 54 - length($name);
                $pad = 1 if $pad <= 0;
-               $entry .= qq(<a\nhref="$href">$name</a>) . (' ' x $pad);
-               $mtime = strftime('%Y-%m-%d %k:%M', gmtime($mtime));
-               $entry .= $mtime . $hsize;
+               $entry = qq(\n<a\nhref="$href">$name</a>) .
+                               (' ' x $pad) .
+                               strftime('%Y-%m-%d %k:%M', gmtime($mtime)) .
+                               sprintf('% 8s', $hsize);
        }
 
        # filter out '.gz' files as long as the mtime matches the
@@ -290,14 +308,16 @@ sub dir_response ($$$) {
        delete(@other{keys %want_gz});
        @entries = ((map { ${$dirs{$_}} } sort keys %dirs),
                        (map { ${$other{$_}} } sort keys %other));
-
        my $path_info_html = ascii_html($path_info);
-       my $body = "<html><head><title>Index of $path_info_html</title>" .
-               ${$self->{style}} .
-               "</head><body><pre>Index of $path_info_html</pre><hr><pre>\n";
-       $body .= join("\n", @entries) . "</pre><hr></body></html>\n";
-       [ 200, [ qw(Content-Type text/html
-                       Content-Length), bytes::length($body) ], [ $body ] ]
+       my @h = qw(Content-Type text/html);
+       my $gzf = gzf_maybe(\@h, $env);
+       print { $gzf->zfh } '<html><head><title>Index of ', $path_info_html,
+               '</title>', ${$self->{style}}, '</head><body><pre>Index of ',
+               $path_info_html, '</pre><hr><pre>', @entries,
+               '</pre><hr></body></html>';
+       my $out = $gzf->zflush;
+       push @h, 'Content-Length', length($out);
+       [ 200, \@h, [ $out ] ]
 }
 
 sub call { # PSGI app endpoint