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