package Plack::Middleware::StaticShared; use strict; use warnings; use parent qw(Plack::Middleware); use Plack::Request; use LWP::Simple qw($ua); use Digest::SHA1 qw(sha1_hex); use DateTime::Format::HTTP; use DateTime; use Path::Class; our $VERSION = '0.04'; __PACKAGE__->mk_accessors(qw(cache base binds verifier)); sub new { my ($class, @args) = @_; my $self = $class->SUPER::new(@args); } sub call { my ($self, $env) = @_; for my $static (@{ $self->binds }) { my $prefix = $static->{prefix}; # Some browsers (eg. Firefox) always access if the url has query string, # so use `:' for parameters my ($version, $files) = ($env->{PATH_INFO} =~ /^$prefix:([^:\s]{1,32}):(.+)$/) or next; my $req = Plack::Request->new($env); my $res = $req->new_response; if ($self->verifier && !$self->verifier->(local $_ = $version, $prefix)) { $res->code(400); return $res->finalize; } my $key = join(':', $version, $files); my $etag = sha1_hex($key); if ($req->header('If-None-Match') || '' eq $etag) { # Browser cache is avaialable but force reloaded by user. $res->code(304); } else { my $content = eval { my $ret = $self->cache->get($etag); if (not defined $ret) { $ret = $self->concat(split /,/, $files); $ret = $static->{filter}->(local $_ = $ret) if $static->{filter}; $self->cache->set($etag => $ret); } $ret; }; if ($@) { $res->code(503); $res->header('Retry-After' => 10); $res->content($@); } else { # Cache control: # IE requires both Last-Modified and Etag to ignore checking updates. $res->code(200); $res->header("Cache-Control" => "public; max-age=315360000; s-maxage=315360000"); $res->header("Expires" => DateTime::Format::HTTP->format_datetime(DateTime->now->add(years => 10))); $res->header("Last-Modified" => DateTime::Format::HTTP->format_datetime(DateTime->from_epoch(epoch => 0))); $res->header("ETag" => $etag); $res->content_type($static->{content_type}); $res->content($content); } } return $res->finalize; } $self->app->($env); } sub concat { my ($self, @files) = @_; my $base = dir($self->base); join("", map { my $file = $base->file($_); $file->resolve; $base->contains($file) ? $file->slurp : ''; } @files ); } 1; __END__ 1; __END__ =head1 NAME Plack::Middleware::StaticShared - concat some static files to one resource =head1 SYNOPSIS use Plack::Builder; use WebService::Google::Closure; builder { enable "StaticShared", cache => Cache::Memcached::Fast->new(servers => [qw/192.168.0.11:11211/]), base => './static/', binds => [ { prefix => '/.shared.js', content_type => 'text/javascript; charset=utf8', filter => sub { WebService::Google::Closure->new(js_code => $_)->compile->code; } }, { prefix => '/.shared.css', content_type => 'text/css; charset=utf8', } ]; verifier => sub { my ($version, $prefix) = @_; $version =~ /v\d/ }, $app; }; And concatnated resources are provided as like following: /.shared.js:v1:/js/foolib.js,/js/barlib.js,/js/app.js => concat following: ./static/js/foolib.js, ./static/js/barlib.js, ./static/js/app.js =head1 DESCRIPTION Plack::Middleware::StaticShared provides resource end point which concat some static files to one resource for reducing http requests. =head1 CONFIGURATIONS =over 4 =item cache (required) A cache object for caching concatnated resource content. =item base (required) Base directory which concatnating resource located in. =item binds (required) Definition of concatnated resources. =item verifier (optional) A subroutine for verifying version string to avoid attacking of cache flooding. =back =head1 AUTHOR cho45 =head1 SEE ALSO L L =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut