Released Static::Simple 0.06:
[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
b1d96e3e 10our $VERSION = '0.06';
d6d29b9b 11
12__PACKAGE__->mk_classdata( qw/_mime_types/ );
b1d96e3e 13__PACKAGE__->mk_accessors( qw/_static_file
14 _apache_mode
15 _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
56 return undef if ( $c->res->status == 404 );
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
78 if ( $c->config->{static}->{use_apache} && $c->_apache_mode ) {
79 my $engine = $c->_apache_mode;
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
113 $c->_mime_types( MIME::Types->new( only_complete => 1 ) );
114 # preload the type index hash so it's not built on the first request
115 $c->_mime_types->create_type_index;
116}
117
118# Search through all included directories for the static file
119# Based on Template Toolkit INCLUDE_PATH code
120sub _locate_static_file {
121 my $c = shift;
122
123 my $path = $c->req->path;
124
125 my @ipaths = @{ $c->config->{static}->{include_path} };
126 my $dpaths;
127 my $count = 64; # maximum number of directories to search
128
129 while ( @ipaths && --$count) {
130 my $dir = shift @ipaths || next;
131
132 if ( ref $dir eq 'CODE' ) {
133 eval { $dpaths = &$dir( $c ) };
134 if ($@) {
135 $c->log->error( "Static::Simple: include_path error: " . $@ );
136 } else {
137 unshift( @ipaths, @$dpaths );
138 next;
139 }
140 } else {
b1d96e3e 141 $dir =~ s/\/$//xms;
d6d29b9b 142 if ( -d $dir && -f $dir . '/' . $path ) {
143 $c->_debug_msg( $dir . "/" . $path )
144 if ( $c->config->{static}->{debug} );
145 return $c->_static_file( $dir . '/' . $path );
146 }
147 }
148 }
149
150 return undef;
151}
152
d6d29b9b 153sub _serve_static {
154 my $c = shift;
155
156 my $path = $c->req->path;
157
158 # abort if running under mod_perl
159 # note that we do not use the Apache method if the user has defined
b1d96e3e 160 # custom MIME types or is using include paths, as Apache would not know
161 # about them
162 APACHE_CHECK:
163 {
164 if ( $c->config->{static}->{use_apache} ) {
165 # check engine version
166 last APACHE_CHECK unless $c->engine =~ /Apache::MP(\d{2})/xms;
167 my $engine = $1;
168
169 # skip if we have user-defined MIME types
170 last APACHE_CHECK if keys %{ $c->config->{static}->{mime_types} };
171
172 # skip if the file is in a user-defined include path
173 last APACHE_CHECK if $c->_static_file
174 ne $c->config->{root} . '/' . $path;
175
176 # check that Apache will serve the correct file
177 if ( $c->apache->document_root ne $c->config->{root} ) {
178 $c->log->warn( "Static::Simple: Your Apache DocumentRoot"
179 . " must be set to " . $c->config->{root}
180 . " to use the Apache feature. Yours is"
181 . " currently " . $c->apache->document_root
182 );
183 }
184 else {
185 $c->_debug_msg( "DECLINED to Apache" )
186 if ( $c->config->{static}->{debug} );
187 $c->_apache_mode( $engine );
188 return undef;
189 }
d6d29b9b 190 }
191 }
192
193 my $type = $c->_ext_to_type;
194
b1d96e3e 195 my $full_path = $c->_static_file;
196 my $stat = stat( $full_path );
d6d29b9b 197
198 # the below code all from C::P::Static
199 if ( $c->req->headers->if_modified_since ) {
200 if ( $c->req->headers->if_modified_since == $stat->mtime ) {
201 $c->res->status( 304 ); # Not Modified
202 $c->res->headers->remove_content_headers;
203 return 1;
204 }
205 }
206
b1d96e3e 207 my $content = read_file( $full_path );
d6d29b9b 208 $c->res->headers->content_type( $type );
209 $c->res->headers->content_length( $stat->size );
210 $c->res->headers->last_modified( $stat->mtime );
211 $c->res->output( $content );
b1d96e3e 212 return 1;
213}
214
215# looks up the correct MIME type for the current file extension
216sub _ext_to_type {
217 my $c = shift;
218
219 my $path = $c->req->path;
220 my $type;
221
222 if ( $path =~ /.*\.(\S{1,})$/xms ) {
223 my $ext = $1;
224 my $user_types = $c->config->{static}->{mime_types};
225 if ( $type = $user_types->{$ext}
226 || $c->_mime_types->mimeTypeOf( $ext ) ) {
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
b1d96e3e 247 if ( !defined $c->_debug_message ) {
248 $c->_debug_message( [] );
249 }
d6d29b9b 250
b1d96e3e 251 if ( $msg ) {
252 push @{ $c->_debug_message }, $msg;
253 }
d6d29b9b 254
255 return $c->_debug_message;
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
294=over 4
295
296=item Forcing directories into static mode
297
298Define a list of top-level directories beneath your 'root' directory that
299should always be served in static mode. Regular expressions may be
300specified using qr//.
301
302 MyApp->config->{static}->{dirs} = [
303 'static',
304 qr/^(images|css)/,
305 ];
306
307=item Including additional directories (experimental!)
308
309You may specify a list of directories in which to search for your static
310files. The directories will be searched in order and will return the first
311file found. Note that your root directory is B<not> automatically added to
312the search path when you specify an include_path. You should use
313MyApp->config->{root} to add it.
314
315 MyApp->config->{static}->{include_path} = [
316 '/path/to/overlay',
317 \&incpath_generator,
318 MyApp->config->{root}
319 ];
320
321With the above setting, a request for the file /images/logo.jpg will search
322for the following files, returning the first one found:
323
324 /path/to/overlay/images/logo.jpg
325 /dynamic/path/images/logo.jpg
326 /your/app/home/root/images/logo.jpg
327
328The include path can contain a subroutine reference to dynamically return a
329list of available directories. This method will receive the $c object as a
330parameter and should return a reference to a list of directories. Errors can
331be reported using die(). This method will be called every time a file is
332requested that appears to be a static file (i.e. it has an extension).
333
334For example:
335
336 sub incpath_generator {
337 my $c = shift;
338
339 if ( $c->session->{customer_dir} ) {
340 return [ $c->session->{customer_dir} ];
341 } else {
342 die "No customer dir defined.";
343 }
344 }
345
346=item Custom MIME types
347
348To override or add to the default MIME types set by the MIME::Types module,
349you may enter your own extension to MIME type mapping.
350
351 MyApp->config->{static}->{mime_types} = {
352 jpg => 'image/jpg',
353 png => 'image/png',
354 };
355
356=item Apache integration and performance
357
358Optionally, when running under mod_perl, Static::Simple can return DECLINED
359on static files to allow Apache to serve the file. A check is first done to
360make sure that Apache's DocumentRoot matches your Catalyst root, and that you
361are not using any custom MIME types or multiple roots. To enable the Apache
362support, you can set the following option.
363
364 MyApp->config->{static}->{use_apache} = 1;
365
366By default this option is disabled because after several benchmarks it
367appears that just serving the file from Catalyst is the better option. On a
3683K file, Catalyst appears to be around 25% faster, and is 42% faster on a 10K
369file. My benchmarking was done using the following 'siege' command, so other
370benchmarks would be welcome!
371
372 siege -u http://server/static/css/10K.css -b -t 1M -c 1
373
374For best static performance, you should still serve your static files directly
375from Apache by defining a Location block similar to the following:
376
377 <Location /static>
378 SetHandler default-handler
379 </Location>
380
381=item Bypassing other plugins
382
383This plugin checks for a static file in the prepare_action stage. If the
384request is for a static file, it will bypass all remaining prepare_action
385steps. This means that by placing Static::Simple before all other plugins,
386they will not execute when a static file is found. This can be helpful by
387skipping session cookie checks for example. Or, if you want some plugins
388to run even on static files, list them before Static::Simple.
389
390Currently, work done by plugins in any other prepare method will execute
391normally.
392
393=item Debugging information
394
395Enable additional debugging information printed in the Catalyst log. This
396is automatically enabled when running Catalyst in -Debug mode.
397
398 MyApp->config->{static}->{debug} = 1;
399
400=back
d6d29b9b 401
402=head1 SEE ALSO
403
b1d96e3e 404L<Catalyst>, L<Catalyst::Plugin::Static>,
405L<http://www.iana.org/assignments/media-types/>
d6d29b9b 406
407=head1 AUTHOR
408
b1d96e3e 409Andy Grundman, <andy@hybridized.org>
d6d29b9b 410
411=head1 THANKS
412
413The authors of Catalyst::Plugin::Static:
414
415 Sebastian Riedel
416 Christian Hansen
417 Marcus Ramberg
418
419For the include_path code from Template Toolkit:
420
421 Andy Wardley
422
423=head1 COPYRIGHT
424
425This program is free software, you can redistribute it and/or modify it under
426the same terms as Perl itself.
427
428=cut