1 package Catalyst::Plugin::Static::Simple;
4 use base qw/Class::Accessor::Fast Class::Data::Inheritable/;
10 our $VERSION = '0.05';
12 __PACKAGE__->mk_classdata( qw/_mime_types/ );
13 __PACKAGE__->mk_accessors( qw/_static_file _apache_mode _debug_message/ );
17 Catalyst::Plugin::Static::Simple - Make serving static pages painless.
22 MyApp->setup( qw/Static::Simple/ );
26 The Static::Simple plugin is designed to make serving static content in your
27 application during development quick and easy, without requiring a single
28 line of code from you.
30 It will detect static files used in your application by looking for file
31 extensions in the URI. By default, you can simply load this plugin and it
32 will immediately begin serving your static files with the correct MIME type.
33 The light-weight MIME::Types module is used to map file extensions to
34 IANA-registered MIME types.
36 Note that actions mapped to paths using periods (.) will still operate
39 You may further tweak the operation by adding configuration options, described
42 =head1 ADVANCED CONFIGURATION
44 Configuration is completely optional and is specified within MyApp->config->{static}.
45 If you use any of these options, the module will probably feel less "simple" to you!
49 =item Forcing directories into static mode
51 Define a list of top-level directories beneath your 'root' directory that
52 should always be served in static mode. Regular expressions may be
55 MyApp->config->{static}->{dirs} = [
60 =item Including additional directories (experimental!)
62 You may specify a list of directories in which to search for your static files. The
63 directories will be searched in order and will return the first file found. Note that
64 your root directory is B<not> automatically added to the search path when
65 you specify an include_path. You should use MyApp->config->{root} to add it.
67 MyApp->config->{static}->{include_path} = [
73 With the above setting, a request for the file /images/logo.jpg will search for the
74 following files, returning the first one found:
76 /path/to/overlay/images/logo.jpg
77 /dynamic/path/images/logo.jpg
78 /your/app/home/root/images/logo.jpg
80 The include path can contain a subroutine reference to dynamically return a list of
81 available directories. This method will receive the $c object as a parameter and
82 should return a reference to a list of directories. Errors can be reported using
83 die(). This method will be called every time a file is requested that appears to
84 be a static file (i.e. it has an extension).
88 sub incpath_generator {
91 if ( $c->session->{customer_dir} ) {
92 return [ $c->session->{customer_dir} ];
94 die "No customer dir defined.";
98 =item Custom MIME types
100 To override or add to the default MIME types set by the MIME::Types module,
101 you may enter your own extension to MIME type mapping.
103 MyApp->config->{static}->{mime_types} = {
108 =item Apache integration and performance
110 Optionally, when running under mod_perl, Static::Simple can return DECLINED
111 on static files to allow Apache to serve the file. A check is first done to
112 make sure that Apache's DocumentRoot matches your Catalyst root, and that you
113 are not using any custom MIME types or multiple roots. To enable the Apache
114 support, you can set the following option.
116 MyApp->config->{static}->{use_apache} = 1;
118 By default this option is disabled because after several benchmarks it
119 appears that just serving the file from Catalyst is the better option. On a 3K
120 file, Catalyst appears to be around 25% faster, and is 42% faster on a 10K file.
121 My benchmarking was done using the following 'siege' command, so other
122 benchmarks would be welcome!
124 siege -u http://server/static/css/10K.css -b -t 1M -c 1
126 For best static performance, you should still serve your static files directly
127 from Apache by defining a Location block similar to the following:
130 SetHandler default-handler
133 =item Debugging information
135 Enable additional debugging information printed in the Catalyst log. This
136 is automatically enabled when running Catalyst in -Debug mode.
138 MyApp->config->{static}->{debug} = 1;
147 my $path = $c->req->path;
149 # is the URI in a static-defined path?
150 foreach my $dir ( @{ $c->config->{static}->{dirs} } ) {
151 my $re = ( $dir =~ /^qr\// ) ? eval $dir : qr/^${dir}/;
152 if ( $path =~ $re ) {
153 if ( $c->_locate_static_file ) {
154 $c->_debug_msg( "from static directory" )
155 if ( $c->config->{static}->{debug} );
156 return $c->_serve_static;
158 $c->_debug_msg( "404: file not found: $path" )
159 if ( $c->config->{static}->{debug} );
160 $c->res->status( 404 );
166 # Does the path have an extension?
167 if ( $path =~ /.*\.(\S{1,})$/ ) {
169 if ( $c->_locate_static_file ) {
170 return $c->_serve_static;
174 return $c->NEXT::dispatch(@_);
180 # display all log messages
181 if ( $c->config->{static}->{debug} && scalar @{$c->_debug_msg} ) {
182 $c->log->debug( "Static::Simple: Serving " .
183 join( " ", @{$c->_debug_msg} )
187 # return DECLINED when under mod_perl
188 if ( $c->config->{static}->{use_apache} && $c->_apache_mode ) {
189 my $engine = $c->_apache_mode;
191 if ( $engine == 13 ) {
192 return Apache::Constants::DECLINED;
193 } elsif ( $engine == 19 ) {
194 return Apache::Const::DECLINED;
195 } elsif ( $engine == 20 ) {
196 return Apache2::Const::DECLINED;
200 if ( $c->res->status =~ /^(1\d\d|[23]04)$/ ) {
201 $c->res->headers->remove_content_headers;
202 return $c->finalize_headers;
204 return $c->NEXT::finalize(@_);
212 $c->config->{static}->{dirs} ||= [];
213 $c->config->{static}->{include_path} ||= [ $c->config->{root} ];
214 $c->config->{static}->{mime_types} ||= {};
215 $c->config->{static}->{use_apache} ||= 0;
216 $c->config->{static}->{debug} ||= $c->debug;
218 # load up a MIME::Types object, only loading types with
219 # at least 1 file extension
220 $c->_mime_types( MIME::Types->new( only_complete => 1 ) );
221 # preload the type index hash so it's not built on the first request
222 $c->_mime_types->create_type_index;
225 # Search through all included directories for the static file
226 # Based on Template Toolkit INCLUDE_PATH code
227 sub _locate_static_file {
230 my $path = $c->req->path;
232 my @ipaths = @{ $c->config->{static}->{include_path} };
234 my $count = 64; # maximum number of directories to search
236 while ( @ipaths && --$count) {
237 my $dir = shift @ipaths || next;
239 if ( ref $dir eq 'CODE' ) {
240 eval { $dpaths = &$dir( $c ) };
242 $c->log->error( "Static::Simple: include_path error: " . $@ );
244 unshift( @ipaths, @$dpaths );
249 if ( -d $dir && -f $dir . '/' . $path ) {
250 $c->_debug_msg( $dir . "/" . $path )
251 if ( $c->config->{static}->{debug} );
252 return $c->_static_file( $dir . '/' . $path );
263 my $path = $c->req->path;
266 if ( $path =~ /.*\.(\S{1,})$/ ) {
268 my $user_types = $c->config->{static}->{mime_types};
269 if ( $type = $user_types->{$ext} || $c->_mime_types->mimeTypeOf( $ext ) ) {
270 $c->_debug_msg( "as $type" )
271 if ( $c->config->{static}->{debug} );
274 $c->_debug_msg( "as text/plain (unknown extension $ext)" )
275 if ( $c->config->{static}->{debug} );
279 $c->_debug_msg( "as text/plain (no extension)" )
280 if ( $c->config->{static}->{debug} );
288 my $path = $c->req->path;
290 # abort if running under mod_perl
291 # note that we do not use the Apache method if the user has defined
292 # custom MIME types or is using include paths, as Apache would not know about them
293 if ( $c->config->{static}->{use_apache} ) {
294 if ( $c->engine =~ /Apache::MP(\d{2})/ &&
295 !keys %{ $c->config->{static}->{mime_types} } &&
296 $c->_static_file eq $c->config->{root} . '/' . $path ) {
298 # check that Apache will serve the correct file
299 if ( $c->apache->document_root ne $c->config->{root} ) {
300 $c->log->warn( "Static::Simple: Your Apache DocumentRoot must be set to " .
301 $c->config->{root} . " to use the Apache feature. Yours is currently " .
302 $c->apache->document_root );
304 $c->_debug_msg( "DECLINED to Apache" )
305 if ( $c->config->{static}->{debug} );
306 $c->_apache_mode( $1 );
312 my $type = $c->_ext_to_type;
314 $path = $c->_static_file;
315 my $stat = stat( $path );
317 # the below code all from C::P::Static
318 if ( $c->req->headers->if_modified_since ) {
319 if ( $c->req->headers->if_modified_since == $stat->mtime ) {
320 $c->res->status( 304 ); # Not Modified
321 $c->res->headers->remove_content_headers;
326 my $content = read_file( $path );
327 $c->res->headers->content_type( $type );
328 $c->res->headers->content_length( $stat->size );
329 $c->res->headers->last_modified( $stat->mtime );
330 $c->res->output( $content );
335 my ( $c, $msg ) = @_;
337 $c->_debug_message( [] ) unless ( $c->_debug_message );
339 push @{ $c->_debug_message }, $msg if $msg;
341 return $c->_debug_message;
346 L<Catalyst>, L<Catalyst::Plugin::Static>, L<http://www.iana.org/assignments/media-types/>
350 Andy Grundman, C<andy@hybridized.org>
354 The authors of Catalyst::Plugin::Static:
360 For the include_path code from Template Toolkit:
366 This program is free software, you can redistribute it and/or modify it under
367 the same terms as Perl itself.