File Coverage

blib/lib/Dancer/Plugin/CDN.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Dancer::Plugin::CDN;
2             {
3             $Dancer::Plugin::CDN::VERSION = '1.001';
4             }
5              
6 2     2   44403 use warnings;
  2         6  
  2         84  
7 2     2   14 use strict;
  2         5  
  2         80  
8              
9 2     2   7249 use Dancer ':syntax';
  2         433111  
  2         16  
10 2     2   3105 use Dancer::Plugin;
  2         2877  
  2         169  
11 2     2   195783 use HTTP::CDN;
  0            
  0            
12             use HTTP::Date;
13              
14              
15             use constant EXPIRES => 315_576_000; # approx 10 years
16              
17              
18             my $cdn;
19              
20              
21             register cdn_url => sub {
22             my($path) = @_;
23              
24             $cdn ||= _init_cdn();
25             return $cdn->resolve($path);
26             };
27              
28              
29             sub _send_cdn_content {
30             $cdn ||= _init_cdn();
31             my ($uri, $hash) = $cdn->unhash_uri(splat);
32              
33             my $info = eval { $cdn->fileinfo($uri) };
34              
35             unless ( $info and $info->{hash} eq $hash ) {
36             status 'not_found';
37             return 'Not Found';
38             }
39              
40             status( 200 );
41             content_type( $info->{mime}->type );
42             header('Last-Modified' => HTTP::Date::time2str($info->{stat}->mtime));
43             header('Expires' => HTTP::Date::time2str(time + EXPIRES));
44             header('Cache-Control' => 'max-age=' . EXPIRES . ', public');
45             return $cdn->filedata($uri);
46              
47             }
48              
49              
50             sub _init_cdn {
51             my $setting = plugin_setting();
52              
53             my $base = $setting->{base} || '/cdn/';
54             my $root = $setting->{root} || setting('public') || 'public';
55              
56             die "CDN root directory does not exist: '$root'\n" unless -d $root;
57              
58             my %args = (
59             root => $root,
60             base => $base,
61             );
62              
63             if( my $plugins = $setting->{plugins} ) {
64             $args{plugins} = $plugins;
65             }
66              
67             return HTTP::CDN->new( %args );
68             }
69              
70              
71             { # Set up route handler to serve responses to rewritten URLS
72              
73             my $base = plugin_setting->{base} || '/cdn/';
74             my($prefix) = $base =~ m{^(?:https?://[^/]+)?(.*)$};
75             my $route = qr/${prefix}(.*)$/;
76              
77             get $route => \&_send_cdn_content;
78             }
79              
80              
81             hook 'before_template_render' => sub {
82             my $tokens = shift;
83             $tokens->{'cdn_url'} = \&cdn_url;
84             };
85              
86              
87             register_plugin;
88              
89             1;
90              
91              
92             =head1 NAME
93              
94             Dancer::Plugin::CDN - Serve static files with unique URLs and far-future expiry
95              
96              
97             =head1 SYNOPSIS
98              
99             use Dancer::Plugin::CDN;
100              
101             # Generate a CDN URL for a static file
102              
103             my $style_sheet = cdn_url('css/style.css'); # e.g.: "/cdn/css/style.B97EA317759D.css"
104              
105             # Or, in a TT2 template:
106              
107             <link rel="stylesheet" href="[% cdn_url('css/style.css') %]" >
108              
109             =head1 DESCRIPTION
110              
111             This plugin generates URLs for your static files that include a content hash so
112             that the URLs will change when the content changes. The plugin also arranges
113             for the files to be served with cache-control and expiry headers to enable the
114             content to be cached by the browser.
115              
116             The real work is performed by the L<HTTP::CDN> module which can also be
117             configured with plugins to minify CSS/JS on-the-fly and also to render LESS to
118             CSS.
119              
120              
121             =head1 FUNCTIONS
122              
123             A single helper function is exported into the caller's namespace. This
124             function is also made available to be called from within your TT2 templates
125             (probably won't work with other template engines).
126              
127             =head2 cdn_url
128              
129             Takes a pathname to a static file (e.g.: C<css/style.css>) and returns a URL
130             with content-hash and configurable CDN prefix added (e.g.:
131             C</cdn/css/style.B97EA317759D.css>);
132              
133              
134             =head1 CONFIGURATION
135              
136             You do not need to configure this module although you may choose to add a
137             section like this to your Dancer config file:
138              
139             plugins:
140             CDN:
141             root: "static"
142             base: "/cdn/"
143             plugins:
144             - "CSS"
145             - "CSS::Minifier::XS"
146              
147             The C<root> setting defines where the static source files can be found. By
148             default this points to Dancer's standard C<public> directory.
149              
150             The C<base> setting is the prefix which will be added to each URL. The default
151             value is C</cdn/>. The plugin will also use this prefix to set up a route
152             handler for serving the static content. This setting can include a hostname
153             e.g.:
154              
155             base: "http://static.example.com/cdn/"
156              
157             The C<plugins> setting should be an array of HTTP::CDN plugin names. The
158             default setting is to enable only the HTTP::CDN::CSS plugin which rewrites
159             URLs (e.g.: for image files) to the CDN scheme.
160              
161              
162             =head1 SUPPORT
163              
164             =over 4
165              
166             =item * Bug reports and feature requests
167              
168             L<https://github.com/grantm/Dancer-Plugin-CDN/issues>
169              
170             =item * Source Code Repository
171              
172             L<http://github.com/grantm/Dancer-Plugin-CDN/>
173              
174             =back
175              
176              
177             =head1 COPYRIGHT AND LICENSE
178              
179             Copyright 2012 Grant McLean C<< <grantm@cpan.org> >>
180              
181             This program is free software; you can redistribute it and/or modify it
182             under the terms of either: the GNU General Public License as published
183             by the Free Software Foundation; or the Artistic License.
184              
185             See http://dev.perl.org/licenses/ for more information.
186              
187              
188             =cut
189              
190             1;
191