File Coverage

blib/lib/Plack/App/MCCS.pm
Criterion Covered Total %
statement 178 190 93.6
branch 84 106 79.2
condition 65 99 65.6
subroutine 24 25 96.0
pod 2 2 100.0
total 353 422 83.6


line stmt bran cond sub pod time code
1             package Plack::App::MCCS;
2              
3             # ABSTRACT: Minify, Compress, Cache-control and Serve static files from Plack applications
4              
5             our $VERSION = "1.000000";
6             $VERSION = eval $VERSION;
7              
8 3     3   27390 use strict;
  3         4  
  3         64  
9 3     3   9 use warnings;
  3         2  
  3         62  
10 3     3   397 use parent qw/Plack::Component/;
  3         215  
  3         9  
11              
12 3     3   20048 use autodie;
  3         32475  
  3         10  
13 3     3   10768 use Cwd ();
  3         3  
  3         46  
14 3     3   9 use Fcntl qw/:flock/;
  3         2  
  3         312  
15 3     3   12 use File::Spec::Unix;
  3         3  
  3         64  
16 3     3   1630 use HTTP::Date;
  3         7689  
  3         166  
17 3     3   1341 use Module::Load::Conditional qw/can_load/;
  3         44438  
  3         148  
18 3     3   1210 use Plack::MIME;
  3         1496  
  3         81  
19 3     3   12 use Plack::Util;
  3         3  
  3         59  
20              
21 3     3   808 use Plack::Util::Accessor qw/root defaults types encoding _can_minify_css _can_minify_js _can_gzip min_cache_dir/;
  3         348  
  3         17  
22              
23             =head1 NAME
24              
25             Plack::App::MCCS - Minify, Compress, Cache-control and Serve static files from Plack applications
26              
27             =head1 EXTENDS
28              
29             L
30              
31             =head1 SYNOPSIS
32              
33             # in your app.psgi:
34             use Plack::Builder;
35             use Plack::App::MCCS;
36              
37             my $app = sub { ... };
38              
39             # be happy with the defaults:
40             builder {
41             mount '/static' => Plack::App::MCCS->new(root => '/path/to/static_files')->to_app;
42             mount '/' => $app;
43             };
44              
45             # or tweak the app to suit your needs:
46             builder {
47             mount '/static' => Plack::App::MCCS->new(
48             root => '/path/to/static_files',
49             min_cache_dir => 'min_cache',
50             defaults => {
51             valid_for => 86400,
52             cache_control => ['private'],
53             },
54             types => {
55             '.htc' => {
56             content_type => 'text/x-component',
57             valid_for => 360,
58             cache_control => ['no-cache', 'must-revalidate'],
59             },
60             },
61             )->to_app;
62             mount '/' => $app;
63             };
64              
65             # or use the supplied middleware
66             builder {
67             enable 'Plack::Middleware::MCCS',
68             path => qr{^/static/},
69             root => '/path/to/static_files'; # all other options are supported
70             $app;
71             };
72              
73             =head1 DESCRIPTION
74              
75             C is a L application that serves static files
76             from a directory. It will prefer serving precompressed versions of files
77             if they exist and the client supports it, and also prefer minified versions
78             of CSS/JS files if they exist.
79              
80             If L is installed, C will also automatically
81             compress files that do not have a precompressed version and save the compressed
82             versions to disk (so it only happens once and not on every request to the
83             same file).
84              
85             If L and/or L are installed,
86             it will also automatically minify CSS/JS files that do not have a preminified
87             version and save them to disk (once again, will only happen once per file).
88              
89             This means C needs to have write privileges to the static files directory.
90             It would be better if files are preminified and precompressed, say automatically
91             in your build process (if such a process exists). However, at some projects
92             where you don't have an automatic build process, it is not uncommon to
93             forget to minify/precompress. That's where automatic minification/compression
94             is useful.
95              
96             Most importantly, C will generate proper Cache Control headers for
97             every file served, including C, C, C
98             and even C (ETags are created automatically, once per file, and saved
99             to disk for future requests). It will appropriately respond with C<304 Not Modified>
100             for requests with headers C or C when
101             these cache validations are fulfilled, without actually having to read the
102             files' contents again.
103              
104             C is active by default, which means that if there are some things
105             you I want it to do, you have to I it not to. This is on purpose,
106             because doing these actions is the whole point of C.
107              
108             =head2 WAIT, AREN'T THERE EXISTING PLACK MIDDLEWARES FOR THAT?
109              
110             Yes and no. A similar functionality can be added to an application by using
111             the following Plack middlewares:
112              
113             =over
114              
115             =item * L or L - will serve static files
116              
117             =item * L - will minify CSS/JS
118              
119             =item * L - will serve precompressed .gz files
120              
121             =item * L - will compress representations with gzip/deflate algorithms
122              
123             =item * L - will create ETags for files
124              
125             =item * L - will handle C and C
126              
127             =item * L - will allow you to add cache control headers manually
128              
129             =back
130              
131             So why wouldn't I just use these middlewares? Here are my reasons:
132              
133             =over
134              
135             =item * C will not minify to disk, but will minify on every
136             request, even to the same file (unless you provide it with a cache, which
137             is not that better). This pointlessly increases the load on the server.
138              
139             =item * C is nice, but it relies on appending C<.gz> to every
140             request and sending it to the app. If the app returns C<404 Not Found>, it sends the request again
141             without the C<.gz> part. This might pollute your logs and I guess two requests
142             to get one file is not better than one request. You can circumvent that with regex matching, but that
143             isn't very comfortable.
144              
145             =item * C will not compress to disk, but do that on every request.
146             So once again, this is a big load on the server for no real reason. It also
147             has a long standing bug where deflate responses fail on Firefox, which is
148             annoying.
149              
150             =item * C will calculate the ETag again on every request.
151              
152             =item * C does not prevent the requested file to be opened
153             for reading even if C<304 Not Modified> is to be returned (since that check is performed later).
154             I'm not sure if it affects performance in anyway, probably not.
155              
156             =item * No possible combination of any of the aformentioned middlewares
157             seems to return proper (and configurable) Cache Control headers, so you
158             need to do that manually, possibly with L,
159             which is not just annoying if different file types have different cache
160             settings, but doesn't even seem to work.
161              
162             =item * I don't really wanna use so many middlewares just for this functionality.
163              
164             =back
165              
166             C attempts to perform all of this faster and better. Read
167             the next section for more info.
168              
169             =head2 HOW DOES MCCS HANDLE REQUESTS?
170              
171             When a request is handed to C, the following process
172             is performed:
173              
174             =over
175              
176             =item 1. Discovery:
177              
178             C will try to find the requested path in the root directory. If the
179             path is not found, C<404 Not Found> is returned. If the path exists but
180             is a directory, C<403 Forbidden> is returned (directory listings might be
181             supported in the future).
182              
183             =item 2. Examination:
184              
185             C will try to find the content type of the file, either by its extension
186             (relying on L for that), or by a specific setting provided
187             to the app by the user (will take precedence). If not found (or file has
188             no extension), C is assumed (which means you should give your
189             files proper extensions if possible).
190              
191             C will also determine for how long to allow browsers/proxy caches/whatever
192             caches to cache the file. By default, it will set a representation as valid
193             for 86400 seconds (i.e. one day). However, this can be changed in two ways:
194             either by setting a different default when creating an instance of the
195             application (see more info at the C method's documentation below),
196             or by setting a specific value for certain file types. Also, C
197             by default sets the C option for the C header,
198             meaning caches are allowed to save responses even when authentication is
199             performed. You can change that the same way.
200              
201             =item 3. Minification
202              
203             If the content type is C or C, C
204             will try to find a preminified version of it on disk (directly, not with
205             a second request). If found, this version will be marked for serving.
206             If not found, and L or L are
207             installed, C will minify the file, save the minified version to disk,
208             and mark it as the version to serve. Future requests to the same file will
209             see the minified version and not minify again.
210              
211             C searches for files that end with C<.min.css> and C<.min.js>, and
212             that's how it creates them too. So if a request comes to C,
213             C will look for C, possibly creating it if not found.
214             The request path remains the same (C) though, even internally.
215             If a request comes to C (which you don't really want when
216             using C), the app will not attempt to minify it again (so you won't
217             get things like C).
218              
219             If C is specified, it will do all its searching and storing of
220             generated minified files within C/C<$min_cache_dir> and ignore minified
221             files outside that directory.
222              
223             =item 4. Compression
224              
225             If the client supports gzip encoding (deflate to be added in the future, probably),
226             as noted with the C header, C will try to find a precompressed
227             version of the file on disk. If found, this version is marked for serving.
228             If not found, and L is installed, C will compress
229             the file, save the gzipped version to disk, and mark it as the version to
230             serve. Future requests to the same file will see the compressed version and
231             not compress again.
232              
233             C searches for files that end with C<.gz>, and that's how it creates
234             them too. So if a request comes to C (and it was minified in
235             the previous step), C will look for C, possibly
236             creating it if not found. The request path remains the same (C) though,
237             even internally.
238              
239             =item 5. Cache Validation
240              
241             If the client provided the C header, C
242             will determine if the file we're serving has been modified after the supplied
243             date, and return C<304 Not Modified> immediately if not.
244              
245             Unless the file has the 'no-store' cache control option, and if the client
246             provided the C header, C will look for
247             a file that has the same name as the file we're going to serve, plus an
248             C<.etag> suffix, such as C for example. If found,
249             the contents of this file is read and compared with the provided ETag. If
250             the two values are equal, C will immediately return C<304 Not Modified>.
251              
252             =item 6. ETagging
253              
254             If an C<.etag> file wasn't found in the previous step (and the file we're
255             serving doesn't have the 'no-store' cache control option), C will create
256             one from the file's inode, last modification date and size. Future requests
257             to the same file will see this ETag file, so it is not created again.
258              
259             =item 7. Headers and Cache-Control
260              
261             C now sets headers, especially cache control headers, as appropriate:
262              
263             C is set to C if a compressed version is returned.
264              
265             C is set with the size of the file in bytes.
266              
267             C is set with the type of the file (if a text file, charset string is appended,
268             e.g. C).
269              
270             C is set with the last modification date of the file in HTTP date format.
271              
272             C is set with the date in which the file will expire (determined in
273             stage 2), in HTTP date format.
274              
275             C is set with the number of seconds the representation is valid for
276             (unless caching of the file is not allowed) and other options (determined in stage 2).
277              
278             C is set with the ETag value (if exists).
279              
280             C is set with C.
281              
282             =item 8. Serving
283              
284             The file handle is returned to the Plack handler/server for serving.
285              
286             =back
287              
288             =head2 HOW DO WEB CACHES WORK ANYWAY?
289              
290             If you need more information on how caches work and cache control headers,
291             read L.
292              
293             =head1 CLASS METHODS
294              
295             =head2 new( %opts )
296              
297             Creates a new instance of this module. C<%opts> I have the following keys:
298              
299             B - the path to the root directory where static files reside.
300              
301             C<%opts> I have the following keys:
302              
303             B - the character set to append to content-type headers when text
304             files are returned. Defaults to UTF-8.
305              
306             B - a hash-ref with some global defaults, the following options
307             are supported:
308              
309             =over
310              
311             =item * B: the default number of seconds caches are allowed to save a response.
312              
313             =item * B: takes an array-ref of options for the C
314             header (all except for C, which is automatically calculated from
315             the resource's C setting).
316              
317             =item * B: give this option a false value (0, empty string, C)
318             if you don't want C to automatically minify CSS/JS files (it will still
319             look for preminified versions though).
320              
321             =item * B: like C, give this option a false value if
322             you don't want C to automatically compress files (it will still look
323             for precompressed versions).
324              
325             =item * B: as above, give this option a false value if you don't want
326             C to automatically create and save ETags. Note that this will mean
327             C will NOT handle ETags at all (so if the client sends the C
328             header, C will ignore it).
329              
330             =back
331              
332             B - For unminified files, by default minified files are generated
333             in the same directory as the original file. If this attribute is specified they
334             are instead generated within C/C<$min_cache_dir>, and minified files
335             outside that directory are ignored, unless requested directly. This can make it
336             easier to filter out generated files when validating a deployment.
337              
338             Giving C, C and C false values is useful during
339             development, when you don't want your project to be "polluted" with all
340             those .gz, .min and .etag files.
341              
342             B - a hash-ref with file extensions that may be served (keys must
343             begin with a dot, so give '.css' and not 'css'). Every extension takes
344             a hash-ref that might have B and B as with the
345             C option, but also B with the content type to return
346             for files with this extension (useful when L doesn't know the
347             content type of a file).
348              
349             If you don't want something to be cached, you need to give the B
350             option (either in C or for a specific file type) a value of either
351             zero, or preferably any number lower than zero, which will cause C
352             to set an C header way in the past. You should also pass the B
353             option C and probably C. When C encounteres the
354             C option, it does not automatically add the C option
355             to the C header.
356              
357             =cut
358              
359             sub new {
360 4     4 1 1062 my $self = shift->SUPER::new(@_);
361              
362             # should we allow minification of files?
363 4 50 66     43 unless ($self->defaults && exists $self->defaults->{minify} && !$self->defaults->{minify}) {
      33        
364             # are we able to minify JavaScript? first attempt to load JavaScript::Minifier::XS,
365             # if unable try JavaScript::Minifier which is pure perl but slower
366 4 50       120 if (can_load(modules => { 'JavaScript::Minifier::XS' => 0.09 })) {
367 4         4681 $self->{_can_minify_js} = 1;
368             }
369              
370             # are we able to minify CSS? like before, try to load XS module first
371 4 50       16 if (can_load(modules => { 'CSS::Minifier::XS' => 0.08 })) {
372 4         4180 $self->{_can_minify_css} = 1;
373             }
374             }
375              
376             # should we allow compression of files?
377 4 50 66     12 unless ($self->defaults && exists $self->defaults->{compress} && !$self->defaults->{compress}) {
      33        
378             # are we able to gzip responses?
379 4 50       34 if (can_load(modules => { 'IO::Compress::Gzip' => undef })) {
380 4         58178 $self->{_can_gzip} = 1;
381             }
382             }
383              
384 4         17 return $self;
385             }
386              
387             =head1 OBJECT METHODS
388              
389             =head2 call( \%env )
390              
391             L automatically calls this method to handle a request. This is where
392             the magic (or disaster) happens.
393              
394             =cut
395              
396             sub call {
397 20     20 1 45129 my ($self, $env) = @_;
398              
399             # find the request file (or return error if occured)
400 20         38 my $file = $self->_locate_file($env->{PATH_INFO});
401 20 100 66     58 return $file if ref $file && ref $file eq 'ARRAY'; # error occured
402              
403             # determine the content type and extension of the file
404 17         33 my ($content_type, $ext) = $self->_determine_content_type($file);
405              
406             # determine cache control for this extension
407 17         152 my ($valid_for, $cache_control, $should_etag) = $self->_determine_cache_control($ext);
408              
409             undef $should_etag
410 17 50 66     27 if $self->defaults && exists $self->defaults->{etag} && !$self->defaults->{etag};
      33        
411              
412             # if this is a CSS/JS file, see if a minified representation of
413             # it exists, unless the file name already has .min.css/.min.js,
414             # in which case we assume it's already minified
415 17 100 100     114 if ($file !~ m/\.min\.(css|js)$/ && ($content_type eq 'text/css' || $content_type eq 'application/javascript')) {
      33        
416 10         10 my $new = $file;
417 10         54 $new =~ s/\.(css|js)$/.min.$1/;
418 10 100       21 $new = $self->_filename_in_min_cache_dir($new) if $self->min_cache_dir;
419 10         45 my $min = $self->_locate_file($new);
420              
421 10         9 my $try_to_minify; # initially undef
422 10 100 66     36 if ($min && !ref $min) {
423             # yes, we found it, but is it still fresh? let's see
424             # when was the source file modified and compare them
425              
426             # $slm = source file last modified date
427 7         14 my $slm = (stat(($self->_full_path($file))[0]))[9];
428             # $mlm = minified file last modified date
429 7         16 my $mlm = (stat(($self->_full_path($min))[0]))[9];
430              
431             # if source file is newer than minified version,
432             # we need to remove the minified version and try
433             # to minify again, otherwise we can simply set the
434             # minified version is the version to serve
435 7 50       14 if ($slm > $mlm) {
436 0         0 unlink(($self->_full_path($min))[0]);
437 0         0 $try_to_minify = 1;
438             } else {
439 7         8 $file = $min;
440             }
441             } else {
442             # minified version does not exist, let's try to
443             # minify ourselves
444 3         4 $try_to_minify = 1;
445             }
446              
447 10 100       19 if ($try_to_minify) {
448             # can we minify ourselves?
449 3 50 66     17 if (($content_type eq 'text/css' && $self->_can_minify_css) || ($content_type eq 'application/javascript' && $self->_can_minify_js)) {
      33        
      66        
450             # open the original file
451 3         17 my $orig = $self->_full_path($file);
452 3 50       7 open(my $ifh, '<:raw', $orig)
453             || return $self->return_403;
454              
455             # add ->path attribute to the file handle
456 3         279 Plack::Util::set_io_path($ifh, Cwd::realpath($orig));
457              
458             # read the file's contents into $css
459 3     3   31 my $body; Plack::Util::foreach($ifh, sub { $body .= $_[0] });
  3         12  
  3         204  
460              
461             # minify contents
462 3 100       175 my $min = $content_type eq 'text/css' ? CSS::Minifier::XS::minify($body) : JavaScript::Minifier::XS::minify($body);
463              
464             # save contents to another file
465 3 50       6 if ($min) {
466 3         7 my $out = $self->_full_path($new);
467 3         7 open(my $ofh, '>:raw', $out);
468 3         245 flock($ofh, LOCK_EX);
469 3 50       96 if ($ofh) {
470 3         17 print $ofh $min;
471 3         5 close $ofh;
472 3         160 $file = $new;
473             }
474             }
475             }
476             }
477             }
478              
479             # search for a gzipped version of this file if the client supports gzip
480 17 100 66     50 if ($env->{HTTP_ACCEPT_ENCODING} && $env->{HTTP_ACCEPT_ENCODING} =~ m/gzip/) {
481 5         11 my $comp = $self->_locate_file($file.'.gz');
482 5         5 my $try_to_compress;
483 5 100 66     18 if ($comp && !ref $comp) {
484             # good, we found a compressed version, but is it
485             # still fresh? like before let's compare its modification
486             # date with the current file marked for serving
487              
488             # $slm = source file last modified date
489 3         4 my $slm = (stat(($self->_full_path($file))[0]))[9];
490             # $clm = compressed file last modified date
491 3         7 my $clm = (stat(($self->_full_path($comp))[0]))[9];
492              
493             # if source file is newer than compressed version,
494             # we need to remove the compressed version and try
495             # to compress again, otherwise we can simply set the
496             # compressed version is the version to serve
497 3 50       6 if ($slm > $clm) {
498 0         0 unlink(($self->_full_path($comp))[0]);
499 0         0 $try_to_compress = 1;
500             } else {
501 3         4 $file = $comp;
502             }
503             } else {
504             # compressed version not found, so let's try to compress
505 2         3 $try_to_compress = 1;
506             }
507              
508 5 100 66     16 if ($try_to_compress && $self->_can_gzip) {
509             # we need to create a gzipped version by ourselves
510 2         10 my $orig = $self->_full_path($file);
511 2         4 my $out = $self->_full_path($file.'.gz');
512 2 50       7 if (IO::Compress::Gzip::gzip($orig, $out)) {
513 2         2886 $file .= '.gz';
514             } else {
515 0         0 warn "failed gzipping $file: $IO::Compress::Gzip::GzipError";
516             }
517             }
518             }
519              
520             # okay, time to serve the file (or not, depending on whether cache
521             # validations exist in the request and are fulfilled)
522 17         35 return $self->_serve_file($env, $file, $content_type, $valid_for, $cache_control, $should_etag);
523             }
524              
525             sub _locate_file {
526 35     35   33 my ($self, $path) = @_;
527              
528             # does request have a sane path?
529 35   50     57 $path ||= '';
530 35 50       57 return $self->_bad_request_400
531             if $path =~ m/\0/;
532              
533 35         52 my ($full, $path_arr) = $self->_full_path($path);
534              
535             # do not allow traveling up in the directory chain
536             return $self->_forbidden_403
537 35 100       39 if grep { $_ eq '..' } @$path_arr;
  44         79  
538              
539 34 100       440 if (-f $full) {
    100          
540             # this is a file, is it readable?
541 27 50       190 return -r $full ? $path : $self->_forbidden_403;
542             } elsif (-d $full) {
543             # this is a directory, we do not allow directory listing (yet)
544 1         2 return $self->_forbidden_403;
545             } else {
546             # not found, return 404
547 6         13 return $self->_not_found_404;
548             }
549             }
550              
551             sub _filename_in_min_cache_dir {
552 1     1   6 my ($self, $file) = @_;
553 1   50     4 my $min_cache_dir = File::Spec->catfile($self->root||".", $self->min_cache_dir);
554 1 50       32 mkdir $min_cache_dir if !-d $min_cache_dir;
555 1         679 $file =~ s@/@%2F@g;
556 1         5 my $new = File::Spec::Unix->catfile($self->min_cache_dir, $file);
557 1         9 return $new;
558             }
559              
560             sub _determine_content_type {
561 17     17   18 my ($self, $file) = @_;
562              
563             # determine extension of the file and see if application defines
564             # a content type for this extension (will even override known types)
565 17         63 my ($ext) = ($file =~ m/(\.[^.]+)$/);
566 17 100 100     53 if ($ext && $self->types && $self->types->{$ext} && $self->types->{$ext}->{content_type}) {
      66        
      66        
567 2         26 return ($self->types->{$ext}->{content_type}, $ext);
568             }
569              
570             # okay, no specific mime defined, let's use Plack::MIME to find it
571             # or fall back to text/plain
572 15   100     187 return (Plack::MIME->mime_type($file) || 'text/plain', $ext)
573             }
574              
575             sub _determine_cache_control {
576 17     17   17 my ($self, $ext) = @_;
577              
578             # MCCS default values
579 17         14 my $valid_for = 86400; # expire in 1 day by default
580 17         20 my @cache_control = ('public'); # allow authenticated caching by default
581              
582             # user provided default values
583             $valid_for = $self->defaults->{valid_for}
584 17 100 66     28 if $self->defaults && defined $self->defaults->{valid_for};
585 1         9 @cache_control = @{$self->defaults->{cache_control}}
586 17 100 66     81 if $self->defaults && defined $self->defaults->{cache_control};
587              
588             # user provided extension specific settings
589 17 100       72 if ($ext) {
590             $valid_for = $self->types->{$ext}->{valid_for}
591 16 100 66     22 if $self->types && $self->types->{$ext} && defined $self->types->{$ext}->{valid_for};
      100        
592 5         49 @cache_control = @{$self->types->{$ext}->{cache_control}}
593 16 100 66     145 if $self->types && $self->types->{$ext} && defined $self->types->{$ext}->{cache_control};
      100        
594             }
595              
596             # unless cache control has no-store, prepend max-age to it
597 17 100       100 my $cache = scalar(grep { $_ eq 'no-store' } @cache_control) ? 0 : 1;
  18         34  
598 17 100       42 unshift(@cache_control, 'max-age='.$valid_for)
599             if $cache;
600              
601 17         36 return ($valid_for, \@cache_control, $cache);
602             }
603              
604             sub _serve_file {
605 17     17   23 my ($self, $env, $path, $content_type, $valid_for, $cache_control, $should_etag) = @_;
606              
607             # if we are serving a text file (including JSON/XML/JavaScript), append character
608             # set to the content type
609 17 100 50     77 $content_type .= '; charset=' . ($self->encoding || 'UTF-8')
610             if $content_type =~ m!^(text/|application/(json|xml|javascript))!;
611              
612             # get the full path of the file
613 17         95 my $file = $self->_full_path($path);
614              
615             # get file statistics
616 17         156 my @stat = stat $file;
617              
618             # try to find the file's etag, unless no-store is on so we don't
619             # care about it
620 17         19 my $etag;
621 17 100       28 if ($should_etag) {
622 16 100 66     276 if (-f "${file}.etag" && -r "${file}.etag") {
    50          
623             # we've found an etag file, and we can read it, but is it
624             # still fresh? let's make sure its last modified date is
625             # later than that of the file itself
626 3 50       23 if ($stat[9] > (stat("${file}.etag"))[9]) {
627             # etag is stale, try to delete it
628 0         0 unlink "${file}.etag";
629             } else {
630             # read the etag file
631 3 50       12 if (open(ETag, '<', "${file}.etag")) {
632 3         190 flock(ETag, LOCK_SH);
633 3         110 $etag = ;
634 3         5 chomp($etag);
635 3         8 close ETag;
636             } else {
637 0         0 warn "Can't open ${file}.etag for reading";
638             }
639             }
640             } elsif (-f "${file}.etag") {
641 0         0 warn "Can't open ${file}.etag for reading";
642             }
643             }
644              
645             # did the client send cache validations?
646 17 100       124 if ($env->{HTTP_IF_MODIFIED_SINCE}) {
647             # okay, client wants to see if resource was modified
648              
649             # IE sends wrong formatted value (i.e. "Thu, 03 Dec 2009 01:46:32 GMT; length=17936")
650             # - taken from Plack::Middleware::ConditionalGET
651 1         2 $env->{HTTP_IF_MODIFIED_SINCE} =~ s/;.*$//;
652 1         3 my $since = HTTP::Date::str2time($env->{HTTP_IF_MODIFIED_SINCE});
653              
654             # if file was modified on or before $since, return 304 Not Modified
655 1 50       57 return $self->_not_modified_304
656             if $stat[9] <= $since;
657             }
658 16 100 66     33 if ($etag && $env->{HTTP_IF_NONE_MATCH} && $etag eq $env->{HTTP_IF_NONE_MATCH}) {
      66        
659 1         3 return $self->_not_modified_304;
660             }
661              
662             # okay, we need to serve the file
663             # open it first
664 15   50     51 open my $fh, '<:raw', $file
665             || return $self->return_403;
666              
667             # add ->path attribute to the file handle
668 15         4910 Plack::Util::set_io_path($fh, Cwd::realpath($file));
669              
670             # did we find an ETag file earlier? if not, let's create one (unless
671             # we shouldn't due to no-store)
672 15 100 100     227 if ($should_etag && !$etag) {
673             # following code based on Plack::Middleware::ETag by Franck Cuny
674             # P::M::ETag creates weak ETag if it sees the resource was
675             # modified less than a second before the request. It seems
676             # like it does that because there's a good chance the resource
677             # will be modified again soon afterwards. I'm not gonna do
678             # that because if MCCS minified/compressed by itself, it will
679             # pretty much always mean the ETag will be created less than a
680             # second after the file was modified, and I know it's not gonna
681             # be modified again soon, so I see no reason to do that here
682              
683             # add inode to etag
684 13         62 $etag .= join('-', sprintf("%x", $stat[2]), sprintf("%x", $stat[9]), sprintf("%x", $stat[7]));
685              
686             # save etag to a file
687 13 50       35 if (open(ETag, '>', "${file}.etag")) {
688 13         1180 flock(ETag, LOCK_EX);
689 13         7749 print ETag $etag;
690 13         27 close ETag;
691             } else {
692 0         0 undef $etag;
693 0         0 warn "Can't open ETag file ${file}.etag for writing";
694             }
695             }
696              
697             # set response headers
698 15         1846 my $headers = [];
699 15 100       50 push(@$headers, 'Content-Encoding' => 'gzip') if $path =~ m/\.gz$/;
700 15         27 push(@$headers, 'Content-Length' => $stat[7]);
701 15         17 push(@$headers, 'Content-Type' => $content_type);
702 15         41 push(@$headers, 'Last-Modified' => HTTP::Date::time2str($stat[9]));
703 15 100       230 push(@$headers, 'Expires' => $valid_for >= 0 ? HTTP::Date::time2str($stat[9]+$valid_for) : HTTP::Date::time2str(0));
704 15         125 push(@$headers, 'Cache-Control' => join(', ', @$cache_control));
705 15 100       36 push(@$headers, 'ETag' => $etag) if $etag;
706 15         17 push(@$headers, 'Vary' => 'Accept-Encoding');
707              
708             # respond
709 15         102 return [200, $headers, $fh];
710             }
711              
712             sub _full_path {
713 82     82   81 my ($self, $path) = @_;
714              
715 82   50     130 my $docroot = $self->root || '.';
716              
717             # break path into chain
718 82         400 my @path = split('/', $path);
719 82 50       115 if (@path) {
720 82 100       130 shift @path if $path[0] eq '';
721             } else {
722 0         0 @path = ('.');
723             }
724              
725 82         316 my $full = File::Spec::Unix->catfile($docroot, @path);
726 82 100       293 return wantarray ? ($full, \@path) : $full;
727             }
728              
729             sub _not_modified_304 {
730 2     2   14 [304, [], []];
731             }
732              
733             sub _bad_request_400 {
734 0     0   0 [400, ['Content-Type' => 'text/plain', 'Content-Length' => 11], ['Bad Request']];
735             }
736              
737             sub _forbidden_403 {
738 2     2   8 [403, ['Content-Type' => 'text/plain', 'Content-Length' => 9], ['Forbidden']];
739             }
740              
741             sub _not_found_404 {
742 6     6   20 [404, ['Content-Type' => 'text/plain', 'Content-Length' => 9], ['Not Found']];
743             }
744              
745             =head1 CAVEATS AND THINGS TO CONSIDER
746              
747             =over
748              
749             =item * You can't tell C to not minify/compress a specific file
750             type yet but only disable minification/compression altogether (in the
751             C setting for the C method).
752              
753             =item * Directory listings are not supported yet (not sure if they will be).
754              
755             =item * Deflate compression is not supported yet (just gzip).
756              
757             =item * Caching middlewares such as L and L
758             don't rely on Cache-Control headers (or so I understand) for
759             their expiration values, which makes them less useful for applications that
760             rely on C. You'll probably be better off with an external cache
761             like L if you want a cache on your application server. Even without
762             a server cache, your application should still appear faster for users due to
763             browser caching (and also server load should be decreased).
764              
765             =item * C requests are not supported. See L if you need that.
766              
767             =item * The app is mounted on a directory and can't be set to only serve
768             requests that match a certain regex. Use the L for that.
769              
770             =back
771              
772             =head1 DIAGNOSTICS
773              
774             This module doesn't throw any exceptions, instead returning HTTP errors
775             for the client and possibly issuing some Cs. The following list should
776             help you to determine some potential problems with C:
777              
778             =over
779              
780             =item C<< "failed gzipping %s: %s" >>
781              
782             This warning is issued when L fails to gzip a file.
783             When it happens, C will simply not return a gzipped representation.
784              
785             =item C<< "Can't open ETag file %s.etag for reading" >>
786              
787             This warning is issued when C can't read an ETag file, probably because
788             it does not have enough permissions. The request will still be fulfilled,
789             but it won't have the C header.
790              
791             =item C<< "Can't open ETag file %s.etag for writing" >>
792              
793             Same as before, but when C can't write an ETag file.
794              
795             =item C<403 Forbidden> is returned for files that exist
796              
797             If a request for a certain file results in a C<403 Forbidden> error, it
798             probably means C has no read permissions for that file.
799              
800             =back
801              
802             =head1 CONFIGURATION AND ENVIRONMENT
803              
804             C requires no configuration files or environment variables.
805              
806             =head1 DEPENDENCIES
807              
808             C B on the following CPAN modules:
809              
810             =over
811              
812             =item * L
813              
814             =item * L
815              
816             =item * L
817              
818             =item * L
819              
820             =item * L
821              
822             =item * L
823              
824             =item * L
825              
826             =item * L (obviously)
827              
828             =back
829              
830             C will use the following modules if they exist, in order
831             to minify/compress files (if they are not installed, C will not be
832             able to minify/compress on its own):
833              
834             =over
835              
836             =item * L
837              
838             =item * L
839              
840             =item * L
841              
842             =back
843              
844             =head1 INCOMPATIBILITIES WITH OTHER MODULES
845              
846             None reported.
847              
848             =head1 BUGS AND LIMITATIONS
849              
850             No bugs have been reported.
851              
852             Please report any bugs or feature requests to
853             C, or through the web interface at
854             L.
855              
856             =head1 SEE ALSO
857              
858             L, L, L, L.
859              
860             =head1 AUTHOR
861              
862             Ido Perlmuter
863              
864             =head1 ACKNOWLEDGMENTS
865              
866             Some of this module's code is based on L by Tatsuhiko Miyagawa
867             and L by Franck Cuny.
868              
869             Christian Walde contributed new features and fixes for the 1.0.0 release.
870              
871             =head1 LICENSE AND COPYRIGHT
872              
873             Copyright (c) 2011-2016, Ido Perlmuter C<< ido@ido50.net >>.
874              
875             This module is free software; you can redistribute it and/or
876             modify it under the same terms as Perl itself, either version
877             5.8.1 or any later version. See L
878             and L.
879              
880             The full text of the license can be found in the
881             LICENSE file included with this module.
882              
883             =head1 DISCLAIMER OF WARRANTY
884              
885             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
886             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
887             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
888             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
889             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
890             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
891             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
892             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
893             NECESSARY SERVICING, REPAIR, OR CORRECTION.
894              
895             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
896             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
897             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
898             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
899             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
900             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
901             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
902             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
903             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
904             SUCH DAMAGES.
905              
906             =cut
907              
908             1;
909             __END__