File Coverage

blib/lib/Plack/App/MCCS.pm
Criterion Covered Total %
statement 169 181 93.3
branch 80 102 78.4
condition 64 97 65.9
subroutine 22 23 95.6
pod 2 2 100.0
total 337 405 83.2


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