Released Static::Simple 0.08 - Added tests for everything except Apache support
[catagits/Catalyst-Plugin-Static-Simple.git] / lib / Catalyst / Plugin / Static / Simple.pm
CommitLineData
d6d29b9b 1package Catalyst::Plugin::Static::Simple;
2
3use strict;
4use base qw/Class::Accessor::Fast Class::Data::Inheritable/;
5use File::Slurp;
6use File::stat;
7use MIME::Types;
8use NEXT;
9
2268e329 10our $VERSION = '0.08';
d6d29b9b 11
2268e329 12__PACKAGE__->mk_classdata( qw/_static_mime_types/ );
b1d96e3e 13__PACKAGE__->mk_accessors( qw/_static_file
2268e329 14 _static_apache_mode
15 _static_debug_message/ );
d6d29b9b 16
b1d96e3e 17# prepare_action is used to first check if the request path is a static file.
18# If so, we skip all other prepare_action steps to improve performance.
19sub prepare_action {
d6d29b9b 20 my $c = shift;
d6d29b9b 21 my $path = $c->req->path;
b1d96e3e 22
d6d29b9b 23 # is the URI in a static-defined path?
24 foreach my $dir ( @{ $c->config->{static}->{dirs} } ) {
b1d96e3e 25 my $re = ( $dir =~ /^qr\//xms ) ? eval $dir : qr/^${dir}/;
26 if ($@) {
27 $c->error( "Error compiling static dir regex '$dir': $@" );
28 }
d6d29b9b 29 if ( $path =~ $re ) {
30 if ( $c->_locate_static_file ) {
31 $c->_debug_msg( "from static directory" )
32 if ( $c->config->{static}->{debug} );
b1d96e3e 33 return;
d6d29b9b 34 } else {
35 $c->_debug_msg( "404: file not found: $path" )
36 if ( $c->config->{static}->{debug} );
37 $c->res->status( 404 );
b1d96e3e 38 return;
d6d29b9b 39 }
40 }
41 }
42
43 # Does the path have an extension?
b1d96e3e 44 if ( $path =~ /.*\.(\S{1,})$/xms ) {
d6d29b9b 45 # and does it exist?
b1d96e3e 46 return if ( $c->_locate_static_file );
d6d29b9b 47 }
48
b1d96e3e 49 return $c->NEXT::prepare_action(@_);
d6d29b9b 50}
51
b1d96e3e 52# dispatch takes the file found during prepare_action and serves it
53sub dispatch {
54 my $c = shift;
55
2268e329 56 return if ( $c->res->status != 200 );
b1d96e3e 57
58 if ( $c->_static_file ) {
59 return $c->_serve_static;
60 }
61 else {
62 return $c->NEXT::dispatch(@_);
63 }
64}
65
66# finalize serves up final header information
d6d29b9b 67sub finalize {
68 my $c = shift;
69
70 # display all log messages
71 if ( $c->config->{static}->{debug} && scalar @{$c->_debug_msg} ) {
72 $c->log->debug( "Static::Simple: Serving " .
73 join( " ", @{$c->_debug_msg} )
74 );
75 }
76
77 # return DECLINED when under mod_perl
2268e329 78 if ( $c->config->{static}->{use_apache} && $c->_static_apache_mode ) {
79 my $engine = $c->_static_apache_mode;
d6d29b9b 80 no strict 'subs';
81 if ( $engine == 13 ) {
82 return Apache::Constants::DECLINED;
b1d96e3e 83 }
84 elsif ( $engine == 19 ) {
d6d29b9b 85 return Apache::Const::DECLINED;
b1d96e3e 86 }
87 elsif ( $engine == 20 ) {
d6d29b9b 88 return Apache2::Const::DECLINED;
89 }
90 }
91
b1d96e3e 92 if ( $c->res->status =~ /^(1\d\d|[23]04)$/xms ) {
d6d29b9b 93 $c->res->headers->remove_content_headers;
94 return $c->finalize_headers;
95 }
b1d96e3e 96
d6d29b9b 97 return $c->NEXT::finalize(@_);
98}
99
100sub setup {
101 my $c = shift;
102
103 $c->NEXT::setup(@_);
104
105 $c->config->{static}->{dirs} ||= [];
106 $c->config->{static}->{include_path} ||= [ $c->config->{root} ];
107 $c->config->{static}->{mime_types} ||= {};
108 $c->config->{static}->{use_apache} ||= 0;
109 $c->config->{static}->{debug} ||= $c->debug;
110
111 # load up a MIME::Types object, only loading types with
112 # at least 1 file extension
2268e329 113 $c->_static_mime_types( MIME::Types->new( only_complete => 1 ) );
114
d6d29b9b 115 # preload the type index hash so it's not built on the first request
2268e329 116 $c->_static_mime_types->create_type_index;
d6d29b9b 117}
118
119# Search through all included directories for the static file
120# Based on Template Toolkit INCLUDE_PATH code
121sub _locate_static_file {
122 my $c = shift;
123
124 my $path = $c->req->path;
125
126 my @ipaths = @{ $c->config->{static}->{include_path} };
127 my $dpaths;
128 my $count = 64; # maximum number of directories to search
129
130 while ( @ipaths && --$count) {
131 my $dir = shift @ipaths || next;
132
133 if ( ref $dir eq 'CODE' ) {
134 eval { $dpaths = &$dir( $c ) };
135 if ($@) {
136 $c->log->error( "Static::Simple: include_path error: " . $@ );
137 } else {
138 unshift( @ipaths, @$dpaths );
139 next;
140 }
141 } else {
b1d96e3e 142 $dir =~ s/\/$//xms;
d6d29b9b 143 if ( -d $dir && -f $dir . '/' . $path ) {
144 $c->_debug_msg( $dir . "/" . $path )
145 if ( $c->config->{static}->{debug} );
146 return $c->_static_file( $dir . '/' . $path );
147 }
148 }
149 }
150
2268e329 151 return;
d6d29b9b 152}
153
d6d29b9b 154sub _serve_static {
155 my $c = shift;
156
157 my $path = $c->req->path;
158
159 # abort if running under mod_perl
160 # note that we do not use the Apache method if the user has defined
b1d96e3e 161 # custom MIME types or is using include paths, as Apache would not know
162 # about them
163 APACHE_CHECK:
164 {
165 if ( $c->config->{static}->{use_apache} ) {
166 # check engine version
167 last APACHE_CHECK unless $c->engine =~ /Apache::MP(\d{2})/xms;
168 my $engine = $1;
169
170 # skip if we have user-defined MIME types
171 last APACHE_CHECK if keys %{ $c->config->{static}->{mime_types} };
172
173 # skip if the file is in a user-defined include path
174 last APACHE_CHECK if $c->_static_file
175 ne $c->config->{root} . '/' . $path;
176
177 # check that Apache will serve the correct file
178 if ( $c->apache->document_root ne $c->config->{root} ) {
179 $c->log->warn( "Static::Simple: Your Apache DocumentRoot"
180 . " must be set to " . $c->config->{root}
181 . " to use the Apache feature. Yours is"
182 . " currently " . $c->apache->document_root
183 );
184 }
185 else {
186 $c->_debug_msg( "DECLINED to Apache" )
187 if ( $c->config->{static}->{debug} );
2268e329 188 $c->_static_apache_mode( $engine );
189 return;
b1d96e3e 190 }
d6d29b9b 191 }
192 }
193
194 my $type = $c->_ext_to_type;
195
b1d96e3e 196 my $full_path = $c->_static_file;
197 my $stat = stat( $full_path );
d6d29b9b 198
199 # the below code all from C::P::Static
200 if ( $c->req->headers->if_modified_since ) {
201 if ( $c->req->headers->if_modified_since == $stat->mtime ) {
202 $c->res->status( 304 ); # Not Modified
203 $c->res->headers->remove_content_headers;
204 return 1;
205 }
206 }
207
b1d96e3e 208 my $content = read_file( $full_path );
d6d29b9b 209 $c->res->headers->content_type( $type );
210 $c->res->headers->content_length( $stat->size );
211 $c->res->headers->last_modified( $stat->mtime );
212 $c->res->output( $content );
b1d96e3e 213 return 1;
214}
215
216# looks up the correct MIME type for the current file extension
217sub _ext_to_type {
218 my $c = shift;
b1d96e3e 219 my $path = $c->req->path;
b1d96e3e 220
221 if ( $path =~ /.*\.(\S{1,})$/xms ) {
222 my $ext = $1;
223 my $user_types = $c->config->{static}->{mime_types};
2268e329 224 my $type = $user_types->{$ext}
225 || $c->_static_mime_types->mimeTypeOf( $ext );
226 if ( $type ) {
b1d96e3e 227 $c->_debug_msg( "as $type" )
228 if ( $c->config->{static}->{debug} );
229 return $type;
230 }
231 else {
232 $c->_debug_msg( "as text/plain (unknown extension $ext)" )
233 if ( $c->config->{static}->{debug} );
234 return 'text/plain';
235 }
236 }
237 else {
238 $c->_debug_msg( 'as text/plain (no extension)' )
239 if ( $c->config->{static}->{debug} );
240 return 'text/plain';
241 }
d6d29b9b 242}
243
244sub _debug_msg {
245 my ( $c, $msg ) = @_;
246
2268e329 247 if ( !defined $c->_static_debug_message ) {
248 $c->_static_debug_message( [] );
b1d96e3e 249 }
d6d29b9b 250
b1d96e3e 251 if ( $msg ) {
2268e329 252 push @{ $c->_static_debug_message }, $msg;
b1d96e3e 253 }
d6d29b9b 254
2268e329 255 return $c->_static_debug_message;
d6d29b9b 256}
b1d96e3e 257
2581;
259__END__
260
261=head1 NAME
262
263Catalyst::Plugin::Static::Simple - Make serving static pages painless.
264
265=head1 SYNOPSIS
266
267 use Catalyst;
268 MyApp->setup( qw/Static::Simple/ );
269
270=head1 DESCRIPTION
271
272The Static::Simple plugin is designed to make serving static content in your
273application during development quick and easy, without requiring a single
274line of code from you.
275
276It will detect static files used in your application by looking for file
277extensions in the URI. By default, you can simply load this plugin and it
278will immediately begin serving your static files with the correct MIME type.
279The light-weight MIME::Types module is used to map file extensions to
280IANA-registered MIME types.
281
282Note that actions mapped to paths using periods (.) will still operate
283properly.
284
285You may further tweak the operation by adding configuration options, described
286below.
287
288=head1 ADVANCED CONFIGURATION
289
290Configuration is completely optional and is specified within
291MyApp->config->{static}. If you use any of these options, the module will
292probably feel less "simple" to you!
293
2268e329 294=head2 Forcing directories into static mode
b1d96e3e 295
296Define a list of top-level directories beneath your 'root' directory that
297should always be served in static mode. Regular expressions may be
298specified using qr//.
299
300 MyApp->config->{static}->{dirs} = [
301 'static',
302 qr/^(images|css)/,
303 ];
304
2268e329 305=head2 Including additional directories (experimental!)
b1d96e3e 306
307You may specify a list of directories in which to search for your static
308files. The directories will be searched in order and will return the first
309file found. Note that your root directory is B<not> automatically added to
310the search path when you specify an include_path. You should use
311MyApp->config->{root} to add it.
312
313 MyApp->config->{static}->{include_path} = [
314 '/path/to/overlay',
315 \&incpath_generator,
316 MyApp->config->{root}
317 ];
318
319With the above setting, a request for the file /images/logo.jpg will search
320for the following files, returning the first one found:
321
322 /path/to/overlay/images/logo.jpg
323 /dynamic/path/images/logo.jpg
324 /your/app/home/root/images/logo.jpg
325
326The include path can contain a subroutine reference to dynamically return a
327list of available directories. This method will receive the $c object as a
328parameter and should return a reference to a list of directories. Errors can
329be reported using die(). This method will be called every time a file is
330requested that appears to be a static file (i.e. it has an extension).
331
332For example:
333
334 sub incpath_generator {
335 my $c = shift;
336
337 if ( $c->session->{customer_dir} ) {
338 return [ $c->session->{customer_dir} ];
339 } else {
340 die "No customer dir defined.";
341 }
342 }
343
2268e329 344=head2 Custom MIME types
b1d96e3e 345
346To override or add to the default MIME types set by the MIME::Types module,
347you may enter your own extension to MIME type mapping.
348
349 MyApp->config->{static}->{mime_types} = {
350 jpg => 'image/jpg',
351 png => 'image/png',
352 };
2268e329 353
354=head2 Apache integration and performance
b1d96e3e 355
356Optionally, when running under mod_perl, Static::Simple can return DECLINED
357on static files to allow Apache to serve the file. A check is first done to
358make sure that Apache's DocumentRoot matches your Catalyst root, and that you
359are not using any custom MIME types or multiple roots. To enable the Apache
360support, you can set the following option.
361
362 MyApp->config->{static}->{use_apache} = 1;
363
364By default this option is disabled because after several benchmarks it
365appears that just serving the file from Catalyst is the better option. On a
3663K file, Catalyst appears to be around 25% faster, and is 42% faster on a 10K
367file. My benchmarking was done using the following 'siege' command, so other
368benchmarks would be welcome!
369
370 siege -u http://server/static/css/10K.css -b -t 1M -c 1
371
372For best static performance, you should still serve your static files directly
373from Apache by defining a Location block similar to the following:
374
375 <Location /static>
376 SetHandler default-handler
377 </Location>
2268e329 378
379=head2 Bypassing other plugins
b1d96e3e 380
381This plugin checks for a static file in the prepare_action stage. If the
382request is for a static file, it will bypass all remaining prepare_action
383steps. This means that by placing Static::Simple before all other plugins,
384they will not execute when a static file is found. This can be helpful by
385skipping session cookie checks for example. Or, if you want some plugins
386to run even on static files, list them before Static::Simple.
387
388Currently, work done by plugins in any other prepare method will execute
389normally.
390
2268e329 391=head2 Debugging information
b1d96e3e 392
393Enable additional debugging information printed in the Catalyst log. This
394is automatically enabled when running Catalyst in -Debug mode.
395
396 MyApp->config->{static}->{debug} = 1;
397
d6d29b9b 398=head1 SEE ALSO
399
b1d96e3e 400L<Catalyst>, L<Catalyst::Plugin::Static>,
401L<http://www.iana.org/assignments/media-types/>
d6d29b9b 402
403=head1 AUTHOR
404
b1d96e3e 405Andy Grundman, <andy@hybridized.org>
d6d29b9b 406
407=head1 THANKS
408
409The authors of Catalyst::Plugin::Static:
410
411 Sebastian Riedel
412 Christian Hansen
413 Marcus Ramberg
414
415For the include_path code from Template Toolkit:
416
417 Andy Wardley
418
419=head1 COPYRIGHT
420
421This program is free software, you can redistribute it and/or modify it under
422the same terms as Perl itself.
423
424=cut