瀏覽代碼

Fix #242: add /favicon.ico route, upgrade icon mongler

This required moving file serving into it's own module.
George Baugh 2 年之前
父節點
當前提交
0f79b0615f
共有 10 個文件被更改,包括 196 次插入158 次删除
  1. 6 0
      bin/favicon_mongler.pl
  2. 8 141
      lib/TCMS.pm
  3. 156 0
      lib/Trog/FileHandler.pm
  4. 14 4
      lib/Trog/Routes/HTML.pm
  5. 3 12
      lib/Trog/Vars.pm
  6. 5 0
      lib/tCMS/Manual.pod
  7. 二進制
      www/img/icon/favicon-167.png
  8. 二進制
      www/img/icon/favicon-32.ico
  9. 二進制
      www/img/icon/favicon-48.png
  10. 4 1
      www/templates/header.tx

+ 6 - 0
bin/favicon_mongler.pl

@@ -6,6 +6,7 @@ use warnings;
 use Cwd            ();
 use File::Basename ();
 use File::Which    ();
+use File::Copy     ();
 
 die "Usage:\n    favicon_mongler.pl /path/to/favicon.svg" unless $ARGV[0];
 my $icon = Cwd::abs_path($ARGV[0]);
@@ -14,6 +15,9 @@ die "Please install inkscape" if !$bin;
 my $dir  = File::Basename::dirname($icon) || die "Can't figure out dir from $icon";
 
 my %files = (
+    32  => 'ico',
+    48  => 'png',
+    167 => 'png',
     180 => 'png',
     192 => 'png',
     512 => 'png',
@@ -25,4 +29,6 @@ foreach my $size ( sort { $b <=> $a } keys(%files) ) {
     print "*** Wrote $dir/favicon-$size.$files{$size} ***\n\n";
 }
 
+File::Copy::copy("$dir/favicon-32.ico", "$dir/favicon.ico");
+
 0;

+ 8 - 141
lib/TCMS.pm

@@ -33,6 +33,7 @@ use Trog::Utils;
 use Trog::Config;
 use Trog::Data;
 use Trog::Vars;
+use Trog::FileHandler;
 
 # Troglodyne philosophy - simple as possible
 
@@ -52,15 +53,6 @@ my %aliases = $data->aliases();
 # This should eventually be pre-filled from DB.
 my %etags;
 
-#1MB chunks
-my $CHUNK_SIZE = 1024000;
-my $CHUNK_SEP  = 'tCMSep666YOLO42069';
-
-#Stuff that isn't in upstream finders
-my %extra_types = (
-    '.docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
-);
-
 =head2 app()
 
 Dispatches requests based on %routes built above.
@@ -104,7 +96,7 @@ sub app {
     if ( $env->{REQUEST_METHOD} eq 'POST' ) {
 
         my $body = HTTP::Body->new( $env->{CONTENT_TYPE}, $env->{CONTENT_LENGTH} );
-        while ( $env->{'psgi.input'}->read( my $buf, $CHUNK_SIZE ) ) {
+        while ( $env->{'psgi.input'}->read( my $buf, $Trog::Vars::CHUNK_SIZE ) ) {
             $body->add($buf);
         }
 
@@ -117,6 +109,8 @@ sub app {
 
     my $path = $env->{PATH_INFO};
     $path = '/index' if $path eq '/';
+    #XXX this is hardcoded in browsers, so just rewrite the path
+    $path = '/img/icon/favicon.ico' if $path eq '/favicon.ico';
 
     # Translate alias paths into their actual path
     $path = $aliases{$path} if exists $aliases{$path};
@@ -186,14 +180,14 @@ sub app {
             map {
                 [ split( /-/, $_ ) ];
 
-                #$tuples[1] //= $tuples[0] + $CHUNK_SIZE;
+                #$tuples[1] //= $tuples[0] + $Trog::Vars::CHUNK_SIZE;
                 #\@tuples
             } split( /,/, $range )
         );
     }
 
-    return _serve( "www/$path",  $start, $streaming, \@ranges, $last_fetch, $deflate ) if -f "www/$path";
-    return _serve( "totp/$path", $start, $streaming, \@ranges, $last_fetch, $deflate ) if -f "totp/$path" && $active_user;
+    return Trog::FileHandler::serve( "www/$path",  $start, $streaming, \@ranges, $last_fetch, $deflate ) if -f "www/$path";
+    return Trog::FileHandler::serve( "totp/$path", $start, $streaming, \@ranges, $last_fetch, $deflate ) if -f "totp/$path" && $active_user;
 
     #Handle regex/capture routes
     if ( !exists $routes{$path} ) {
@@ -305,7 +299,7 @@ sub _static ( $path, $start, $streaming, $last_fetch = 0 ) {
 
             #push(@headers, 'Content-Length' => $sz);
             my $writer = $responder->( [ $code, [%$headers_parsed] ] );
-            while ( $fh->read( my $buf, $CHUNK_SIZE ) ) {
+            while ( $fh->read( my $buf, $Trog::Vars::CHUNK_SIZE ) ) {
                 $writer->write($buf);
             }
             close $fh;
@@ -318,131 +312,4 @@ sub _static ( $path, $start, $streaming, $last_fetch = 0 ) {
     return [ 403, [ 'Content-Type' => $Trog::Vars::content_types{plain} ], ["STAY OUT YOU RED MENACE"] ];
 }
 
-sub _range ( $fh, $ranges, $sz, %headers ) {
-
-    # Set mode
-    my $primary_ct   = "Content-Type: $headers{'Content-type'}";
-    my $is_multipart = scalar(@$ranges) > 1;
-    if ($is_multipart) {
-        $headers{'Content-type'} = "multipart/byteranges; boundary=$CHUNK_SEP";
-    }
-    my $code = 206;
-
-    my $fc = '';
-
-    # Calculate the content-length up-front.  We have to fix unspecified lengths first, and reject bad requests.
-    foreach my $range (@$ranges) {
-        $range->[1] //= $sz - 1;
-        return [ 416, [%headers], ["Requested range not satisfiable"] ] if $range->[0] > $sz || $range->[0] < 0 || $range->[1] < 0 || $range->[0] > $range->[1];
-    }
-    $headers{'Content-Length'} = List::Util::sum( map { my $arr = $_; $arr->[1] + 1, -$arr->[0] } @$ranges );
-
-    #XXX Add the entity header lengths to the value - should hash-ify this to DRY
-    if ($is_multipart) {
-        foreach my $range (@$ranges) {
-            $headers{'Content-Length'} += length("$fc--$CHUNK_SEP\n$primary_ct\nContent-Range: bytes $range->[0]-$range->[1]/$sz\n\n");
-            $fc = "\n";
-        }
-        $headers{'Content-Length'} += length("\n--$CHUNK_SEP\--\n");
-        $fc = '';
-    }
-
-    return sub {
-        my $responder = shift;
-        my $writer;
-
-        foreach my $range (@$ranges) {
-            $headers{'Content-Range'} = "bytes $range->[0]-$range->[1]/$sz" unless $is_multipart;
-            $writer //= $responder->( [ $code, [%headers] ] );
-            $writer->write("$fc--$CHUNK_SEP\n$primary_ct\nContent-Range: bytes $range->[0]-$range->[1]/$sz\n\n") if $is_multipart;
-            $fc = "\n";
-
-            my $len = List::Util::min( $sz, $range->[1] + 1 ) - $range->[0];
-
-            $fh->seek( $range->[0], 0 );
-            while ($len) {
-                $fh->read( my $buf, List::Util::min( $len, $CHUNK_SIZE ) );
-                $writer->write($buf);
-
-                # Adjust for amount written
-                $len = List::Util::max( $len - $CHUNK_SIZE, 0 );
-            }
-        }
-        $fh->close();
-        $writer->write("\n--$CHUNK_SEP\--\n") if $is_multipart;
-        $writer->close;
-    };
-}
-
-sub _serve ( $path, $start, $streaming, $ranges, $last_fetch = 0, $deflate = 0 ) {
-    my $mf  = Mojo::File->new($path);
-    my $ext = '.' . $mf->extname();
-    my $ft;
-    if ($ext) {
-        $ft = Plack::MIME->mime_type($ext) if $ext;
-        $ft ||= $extra_types{$ext}         if exists $extra_types{$ext};
-    }
-    $ft ||= $Trog::Vars::content_types{plain};
-
-    my $ct      = 'Content-type';
-    my @headers = ( $ct => $ft );
-
-    #TODO use static Cache-Control for everything but JS/CSS?
-    push( @headers, 'Cache-control' => $Trog::Vars::cache_control{revalidate} );
-
-    push( @headers, 'Accept-Ranges' => 'bytes' );
-
-    my $mt         = ( stat($path) )[9];
-    my $sz         = ( stat(_) )[7];
-    my @gm         = gmtime($mt);
-    my $now_string = strftime( "%a, %d %b %Y %H:%M:%S GMT", @gm );
-    my $code       = $mt > $last_fetch ? 200 : 304;
-
-    push( @headers, "Last-Modified" => $now_string );
-    push( @headers, 'Vary'          => 'Accept-Encoding' );
-
-    if ( open( my $fh, '<', $path ) ) {
-        return _range( $fh, $ranges, $sz, @headers ) if @$ranges && $streaming;
-
-        # Transfer-encoding: chunked
-        return sub {
-            my $responder = shift;
-            push( @headers, 'Content-Length' => $sz );
-            my $writer = $responder->( [ $code, \@headers ] );
-            while ( $fh->read( my $buf, $CHUNK_SIZE ) ) {
-                $writer->write($buf);
-            }
-            close $fh;
-            $writer->close;
-          }
-          if $streaming && $sz > $CHUNK_SIZE;
-
-        #Return data in the event the caller does not support deflate
-        if ( !$deflate ) {
-            push( @headers, "Content-Length" => $sz );
-
-            # Append server-timing headers
-            my $tot = tv_interval($start) * 1000;
-            push( @headers, 'Server-Timing' => "file;dur=$tot" );
-
-            return [ $code, \@headers, $fh ];
-        }
-
-        #Compress everything less than 1MB
-        push( @headers, "Content-Encoding" => "gzip" );
-        my $dfh;
-        IO::Compress::Gzip::gzip( $fh => \$dfh );
-        print $IO::Compress::Gzip::GzipError if $IO::Compress::Gzip::GzipError;
-        push( @headers, "Content-Length" => length($dfh) );
-
-        # Append server-timing headers
-        my $tot = tv_interval($start) * 1000;
-        push( @headers, 'Server-Timing' => "file;dur=$tot" );
-
-        return [ $code, \@headers, [$dfh] ];
-    }
-
-    return [ 403, [ $ct => $Trog::Vars::content_types{plain} ], ["STAY OUT YOU RED MENACE"] ];
-}
-
 1;

+ 156 - 0
lib/Trog/FileHandler.pm

@@ -0,0 +1,156 @@
+package Trog::FileHandler;
+
+use strict;
+use warnings;
+
+no warnings 'experimental';
+use feature qw{signatures};
+
+use POSIX qw{strftime};
+use Mojo::File;
+use Plack::MIME;
+use IO::Compress::Gzip;
+use Time::HiRes qw{tv_interval};
+
+use Trog::Vars;
+
+#TODO consider integrating libfile
+#Stuff that isn't in upstream finders
+my %extra_types = (
+    '.docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+);
+
+=head2 serve
+
+Serve a file, with options to stream and cache the output.
+
+=cut
+
+sub serve ( $path, $start, $streaming, $ranges, $last_fetch = 0, $deflate = 0 ) {
+    my $mf  = Mojo::File->new($path);
+    my $ext = '.' . $mf->extname();
+    my $ft;
+    if ($ext) {
+        $ft = Plack::MIME->mime_type($ext) if $ext;
+        $ft ||= $extra_types{$ext}         if exists $extra_types{$ext};
+    }
+    $ft ||= $Trog::Vars::content_types{plain};
+
+    my $ct      = 'Content-type';
+    my @headers = ( $ct => $ft );
+
+    #TODO use static Cache-Control for everything but JS/CSS?
+    push( @headers, 'Cache-control' => $Trog::Vars::cache_control{revalidate} );
+
+    push( @headers, 'Accept-Ranges' => 'bytes' );
+
+    my $mt         = ( stat($path) )[9];
+    my $sz         = ( stat(_) )[7];
+    my @gm         = gmtime($mt);
+    my $now_string = strftime( "%a, %d %b %Y %H:%M:%S GMT", @gm );
+    my $code       = $mt > $last_fetch ? 200 : 304;
+
+    push( @headers, "Last-Modified" => $now_string );
+    push( @headers, 'Vary'          => 'Accept-Encoding' );
+
+    if ( open( my $fh, '<', $path ) ) {
+        return _range( $fh, $ranges, $sz, @headers ) if @$ranges && $streaming;
+
+        # Transfer-encoding: chunked
+        return sub {
+            my $responder = shift;
+            push( @headers, 'Content-Length' => $sz );
+            my $writer = $responder->( [ $code, \@headers ] );
+            while ( $fh->read( my $buf, $Trog::Vars::CHUNK_SIZE ) ) {
+                $writer->write($buf);
+            }
+            close $fh;
+            $writer->close;
+          }
+          if $streaming && $sz > $Trog::Vars::CHUNK_SIZE;
+
+        #Return data in the event the caller does not support deflate
+        if ( !$deflate ) {
+            push( @headers, "Content-Length" => $sz );
+
+            # Append server-timing headers
+            my $tot = tv_interval($start) * 1000;
+            push( @headers, 'Server-Timing' => "file;dur=$tot" );
+
+            return [ $code, \@headers, $fh ];
+        }
+
+        #Compress everything less than 1MB
+        push( @headers, "Content-Encoding" => "gzip" );
+        my $dfh;
+        IO::Compress::Gzip::gzip( $fh => \$dfh );
+        print $IO::Compress::Gzip::GzipError if $IO::Compress::Gzip::GzipError;
+        push( @headers, "Content-Length" => length($dfh) );
+
+        # Append server-timing headers
+        my $tot = tv_interval($start) * 1000;
+        push( @headers, 'Server-Timing' => "file;dur=$tot" );
+
+        return [ $code, \@headers, [$dfh] ];
+    }
+
+    return [ 403, [ $ct => $Trog::Vars::content_types{plain} ], ["STAY OUT YOU RED MENACE"] ];
+}
+
+sub _range ( $fh, $ranges, $sz, %headers ) {
+
+    # Set mode
+    my $primary_ct   = "Content-Type: $headers{'Content-type'}";
+    my $is_multipart = scalar(@$ranges) > 1;
+    if ($is_multipart) {
+        $headers{'Content-type'} = "multipart/byteranges; boundary=$Trog::Vars::CHUNK_SEP";
+    }
+    my $code = 206;
+
+    my $fc = '';
+
+    # Calculate the content-length up-front.  We have to fix unspecified lengths first, and reject bad requests.
+    foreach my $range (@$ranges) {
+        $range->[1] //= $sz - 1;
+        return [ 416, [%headers], ["Requested range not satisfiable"] ] if $range->[0] > $sz || $range->[0] < 0 || $range->[1] < 0 || $range->[0] > $range->[1];
+    }
+    $headers{'Content-Length'} = List::Util::sum( map { my $arr = $_; $arr->[1] + 1, -$arr->[0] } @$ranges );
+
+    #XXX Add the entity header lengths to the value - should hash-ify this to DRY
+    if ($is_multipart) {
+        foreach my $range (@$ranges) {
+            $headers{'Content-Length'} += length("$fc--$Trog::Vars::CHUNK_SEP\n$primary_ct\nContent-Range: bytes $range->[0]-$range->[1]/$sz\n\n");
+            $fc = "\n";
+        }
+        $headers{'Content-Length'} += length("\n--$Trog::Vars::CHUNK_SEP\--\n");
+        $fc = '';
+    }
+
+    return sub {
+        my $responder = shift;
+        my $writer;
+
+        foreach my $range (@$ranges) {
+            $headers{'Content-Range'} = "bytes $range->[0]-$range->[1]/$sz" unless $is_multipart;
+            $writer //= $responder->( [ $code, [%headers] ] );
+            $writer->write("$fc--$Trog::Vars::CHUNK_SEP\n$primary_ct\nContent-Range: bytes $range->[0]-$range->[1]/$sz\n\n") if $is_multipart;
+            $fc = "\n";
+
+            my $len = List::Util::min( $sz, $range->[1] + 1 ) - $range->[0];
+
+            $fh->seek( $range->[0], 0 );
+            while ($len) {
+                $fh->read( my $buf, List::Util::min( $len, $Trog::Vars::CHUNK_SIZE ) );
+                $writer->write($buf);
+
+                # Adjust for amount written
+                $len = List::Util::max( $len - $Trog::Vars::CHUNK_SIZE, 0 );
+            }
+        }
+        $fh->close();
+        $writer->write("\n--$Trog::Vars::CHUNK_SEP\--\n") if $is_multipart;
+        $writer->close;
+    };
+}
+
+1;

+ 14 - 4
lib/Trog/Routes/HTML.pm

@@ -24,6 +24,7 @@ use Trog::Utils;
 use Trog::Config;
 use Trog::Auth;
 use Trog::Data;
+use Trog::FileHandler;
 
 my $conf         = Trog::Config::get();
 my $template_dir = 'www/templates';
@@ -176,6 +177,10 @@ our %routes = (
         callback => \&Trog::Routes::HTML::avatars,
         data     => { tag => ['about'] },
     },
+    '/favicon.ico' => {
+        method   => 'GET',
+        callback => \&Trog::Routes::HTML::icon,
+    },
 );
 
 # Grab theme routes
@@ -1244,13 +1249,12 @@ sub _rss ( $query, $posts ) {
         lastBuildDate => $now,
     );
 
-    #TODO configurability
     $rss->image(
         title       => $query->{domain},
-        url         => "$td/img/icon/favicon.ico",
+        url         => "/favicon.ico",
         link        => "http://$query->{domain}",
-        width       => 88,
-        height      => 31,
+        width       => 32,
+        height      => 32,
         description => "$query->{domain} favicon",
     );
 
@@ -1484,4 +1488,10 @@ sub save_render ( $vars, $body, %headers ) {
     close $fh;
 }
 
+# basically a file rewrite rule for themes
+sub icon ($query) {
+    my $path = $query->{route};
+    return Trog::FileHandler::serve("$td/img/icon/$path");
+}
+
 1;

+ 3 - 12
lib/Trog/Vars.pm

@@ -3,15 +3,6 @@ package Trog::Vars;
 use strict;
 use warnings;
 
-our %content_types = (
-    plain => "text/plain;",
-    html  => "text/html; charset=UTF-8",
-    json  => "application/json;",
-    blob  => "application/octet-stream;",
-);
-
-our %cache_control = (
-    revalidate => "no-cache, max-age=0",
-    nocache    => "no-store",
-    static     => "public, max-age=604800, immutable",
-);
+#1MB chunks
+our $CHUNK_SEP  = 'tCMSep666YOLO42069';
+our $CHUNK_SIZE = 1024000;

+ 5 - 0
lib/tCMS/Manual.pod

@@ -59,3 +59,8 @@ The module controlling this is L<Trog::Auth>.
 
 Themes are subdirectories of /themes, which mirror the structure of www/ internally.
 Stylesheets are included after the mainline ones, so that your styles will override the default.
+
+=head3 Theme Icons
+
+You will want your theme icon to be in the img/icon directory, make sure it's an SVG named 'favicon.svg'.
+From there, run bin/favicon_mongler.pl $PATH_TO_YOUR_FAVICON_SVG

二進制
www/img/icon/favicon-167.png


二進制
www/img/icon/favicon-32.ico


二進制
www/img/icon/favicon-48.png


+ 4 - 1
www/templates/header.tx

@@ -5,7 +5,10 @@
         <meta charset="utf-8" />
 
         <link rel="icon" type="image/svg+xml" href="<: $theme_dir :>/img/icon/favicon.svg">
-        <link rel="apple-touch-icon" href="<: $theme_dir :>/img/icon/favicon-180.png">
+        <link rel="apple-touch-icon" type="image/png" sizes="167x167" href="<: $theme_dir :>/img/icon/favicon-167.png">
+        <link rel="apple-touch-icon" type="image/png" sizes="180x180" href="<: $theme_dir :>/img/icon/favicon-180.png">
+        <link rel="icon" type="image/png" sizes="48x48" href="<: $theme_dir :>/img/icon/favicon-48.png">
+        <link rel="icon" type="image/png" sizes="192x192" href="<: $theme_dir :>/img/icon/favicon-192.png">
         <link rel="manifest" href="/api/webmanifest">
 
         : if ($author) {