5abcc768f6ace8f193aad1e83c9d01b564143077
[catagits/Catalyst-Plugin-Static-Simple.git] / lib / Catalyst / Plugin / Static / Simple.pm
1 package Catalyst::Plugin::Static::Simple;
2
3 use strict;
4 use base qw/Class::Accessor::Fast Class::Data::Inheritable/;
5 use File::Slurp;
6 use File::stat;
7 use MIME::Types;
8 use NEXT;
9
10 our $VERSION = '0.05';
11
12 __PACKAGE__->mk_classdata( qw/_mime_types/ );
13 __PACKAGE__->mk_accessors( qw/_static_file _apache_mode _debug_message/ );
14
15 =head1 NAME
16
17 Catalyst::Plugin::Static::Simple - Make serving static pages painless.
18
19 =head1 SYNOPSIS
20
21     use Catalyst;
22     MyApp->setup( qw/Static::Simple/ );
23
24 =head1 DESCRIPTION
25
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.
29
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.
35
36 Note that actions mapped to paths using periods (.) will still operate
37 properly.
38
39 You may further tweak the operation by adding configuration options, described
40 below.
41
42 =head1 ADVANCED CONFIGURATION
43
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!
46
47 =over 4
48
49 =item Forcing directories into static mode
50
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
53 specified using qr//.
54
55     MyApp->config->{static}->{dirs} = [
56         'static',
57         qr/^(images|css)/,
58     ];
59
60 =item Including additional directories (experimental!)
61
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.
66
67     MyApp->config->{static}->{include_path} = [
68         '/path/to/overlay',
69         \&incpath_generator,
70         MyApp->config->{root}
71     ];
72     
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:
75
76     /path/to/overlay/images/logo.jpg
77     /dynamic/path/images/logo.jpg
78     /your/app/home/root/images/logo.jpg
79     
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).
85
86 For example:
87
88     sub incpath_generator {
89         my $c = shift;
90         
91         if ( $c->session->{customer_dir} ) {
92             return [ $c->session->{customer_dir} ];
93         } else {
94             die "No customer dir defined.";
95         }
96     }
97
98 =item Custom MIME types
99
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. 
102
103     MyApp->config->{static}->{mime_types} = {
104         jpg => 'image/jpg',
105         png => 'image/png',
106     };
107     
108 =item Apache integration and performance
109
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.
115
116     MyApp->config->{static}->{use_apache} = 1;
117     
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!
123
124     siege -u http://server/static/css/10K.css -b -t 1M -c 1
125
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:
128
129     <Location /static>
130         SetHandler default-handler
131     </Location>
132
133 =item Debugging information
134
135 Enable additional debugging information printed in the Catalyst log.  This
136 is automatically enabled when running Catalyst in -Debug mode.
137
138     MyApp->config->{static}->{debug} = 1;
139
140 =back
141
142 =cut
143
144 sub dispatch {
145     my $c = shift;
146     
147     my $path = $c->req->path;
148     
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;
157             } else {
158                 $c->_debug_msg( "404: file not found: $path" )
159                     if ( $c->config->{static}->{debug} );
160                 $c->res->status( 404 );
161                 return 0;
162             }
163         }
164     }
165     
166     # Does the path have an extension?
167     if ( $path =~ /.*\.(\S{1,})$/ ) {
168         # and does it exist?
169         if ( $c->_locate_static_file ) {
170             return $c->_serve_static;
171         }
172     }
173     
174     return $c->NEXT::dispatch(@_);
175 }
176
177 sub finalize {
178     my $c = shift;
179     
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} )
184         );
185     }
186     
187     # return DECLINED when under mod_perl
188     if ( $c->config->{static}->{use_apache} && $c->_apache_mode ) {
189         my $engine = $c->_apache_mode;
190         no strict 'subs';
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;
197         }
198     }
199     
200     if ( $c->res->status =~ /^(1\d\d|[23]04)$/ ) {
201         $c->res->headers->remove_content_headers;
202         return $c->finalize_headers;
203     }
204     return $c->NEXT::finalize(@_);
205 }
206
207 sub setup {
208     my $c = shift;
209     
210     $c->NEXT::setup(@_);
211     
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;
217     
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;
223 }
224
225 # Search through all included directories for the static file
226 # Based on Template Toolkit INCLUDE_PATH code
227 sub _locate_static_file {
228     my $c = shift;
229     
230     my $path = $c->req->path;
231     
232     my @ipaths = @{ $c->config->{static}->{include_path} };
233     my $dpaths;
234     my $count = 64; # maximum number of directories to search
235     
236     while ( @ipaths && --$count) {
237         my $dir = shift @ipaths || next;
238         
239         if ( ref $dir eq 'CODE' ) {
240             eval { $dpaths = &$dir( $c ) };
241             if ($@) {
242                 $c->log->error( "Static::Simple: include_path error: " . $@ );
243             } else {
244                 unshift( @ipaths, @$dpaths );
245                 next;
246             }
247         } else {
248             $dir =~ s/\/$//;
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 );
253             }
254         }
255     }
256     
257     return undef;
258 }
259
260 sub _ext_to_type {
261     my $c = shift;
262     
263     my $path = $c->req->path;
264     my $type;
265     
266     if ( $path =~ /.*\.(\S{1,})$/ ) {
267         my $ext = $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} );            
272             return $type;
273         } else {
274             $c->_debug_msg( "as text/plain (unknown extension $ext)" )
275                 if ( $c->config->{static}->{debug} );
276             return 'text/plain';
277         }
278     } else {
279         $c->_debug_msg( "as text/plain (no extension)" )
280             if ( $c->config->{static}->{debug} );
281         return 'text/plain';
282     }
283 }
284
285 sub _serve_static {
286     my $c = shift;
287     
288     my $path = $c->req->path;    
289     
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 ) {
297                  
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 );
303                  } else {
304                      $c->_debug_msg( "DECLINED to Apache" )
305                         if ( $c->config->{static}->{debug} );          
306                      $c->_apache_mode( $1 );
307                      return undef;
308                  }
309         }
310     }
311     
312     my $type = $c->_ext_to_type;
313     
314     $path = $c->_static_file;
315     my $stat = stat( $path );
316
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;
322             return 1;
323         }
324     }
325
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 );
331     return 1;  
332 }
333
334 sub _debug_msg {
335     my ( $c, $msg ) = @_;
336     
337     $c->_debug_message( [] ) unless ( $c->_debug_message );
338     
339     push @{ $c->_debug_message }, $msg if $msg;
340     
341     return $c->_debug_message;
342 }
343     
344 =head1 SEE ALSO
345
346 L<Catalyst>, L<Catalyst::Plugin::Static>, L<http://www.iana.org/assignments/media-types/>
347
348 =head1 AUTHOR
349
350 Andy Grundman, C<andy@hybridized.org>
351
352 =head1 THANKS
353
354 The authors of Catalyst::Plugin::Static:
355
356     Sebastian Riedel
357     Christian Hansen
358     Marcus Ramberg
359
360 For the include_path code from Template Toolkit:
361
362     Andy Wardley
363
364 =head1 COPYRIGHT
365
366 This program is free software, you can redistribute it and/or modify it under
367 the same terms as Perl itself.
368
369 =cut
370
371 1;