File Coverage

blib/lib/Catalyst/Controller/AutoAssets.pm
Criterion Covered Total %
statement 36 40 90.0
branch 3 6 50.0
condition 2 6 33.3
subroutine 10 11 90.9
pod 0 3 0.0
total 51 66 77.2


line stmt bran cond sub pod time code
1             package Catalyst::Controller::AutoAssets;
2 4     4   830109 use strict;
  4         8  
  4         100  
3 4     4   15 use warnings;
  4         7  
  4         159  
4              
5             our $VERSION = '0.40';
6              
7 4     4   14 use Moose;
  4         22  
  4         26  
8 4     4   17054 use namespace::autoclean;
  4         6  
  4         37  
9             require Module::Runtime;
10              
11 4     4   280 BEGIN { extends 'Catalyst::Controller' }
12              
13             has 'type', is => 'ro', isa => 'Str', required => 1;
14             has 'no_logs', is => 'rw', isa => 'Bool', default => sub {1};
15              
16             has '_module_version', is => 'ro', isa => 'Str', default => $VERSION;
17              
18             # Save the build params (passed to constructor)
19             has '_build_params', is => 'ro', isa => 'HashRef', required => 1;
20             around BUILDARGS => sub {
21             my ($orig, $class, $c, @args) = @_;
22             my %params = (ref($args[0]) eq 'HASH') ? %{ $args[0] } : @args; # <-- arg as hash or hashref
23             $params{_build_params} = {%params};
24             return $class->$orig($c,\%params);
25             };
26              
27             # The Handler (which is determined by the asset type) is
28             # where most of the actual work gets done:
29             has '_Handler' => (
30             is => 'ro', init_arg => undef, lazy => 1,
31             does => 'Catalyst::Controller::AutoAssets::Handler',
32             handles => [qw(request asset_path html_head_tags)],
33             default => sub {
34             my $self = shift;
35             my $class = $self->_resolve_handler_class($self->type);
36             return $class->new({
37             %{$self->_build_params},
38             Controller => $self
39             });
40             }
41             );
42              
43             # Delegate all other function calls to the Handler to support future
44             # Handler classes and new methods
45             our $AUTOLOAD;
46             sub AUTOLOAD {
47 0     0   0 $AUTOLOAD =~ /([^:]+)$/;
48 0         0 eval "sub $1 { (shift)->_Handler->$1(\@_); }";
49 0         0 goto $_[0]->can($1);
50             }
51              
52             sub _resolve_handler_class {
53 4     4   6 my $self = shift;
54 4         7 my $class = shift;
55            
56             # legacy, original, lower-case, built-in type names:
57 4         17 my %type_aliases = ( css => 'CSS', js => 'JS', directory => 'Directory' );
58 4 50       15 $class = $type_aliases{$class} if (exists $type_aliases{$class});
59            
60             # Allow absolute class names using '+' prefix:
61 4 50       18 $class = $class =~ /^\+(.*)$/ ? $1
62             : "Catalyst::Controller::AutoAssets::Handler::$class";
63 4         21 Module::Runtime::require_module($class);
64 4         30 return $class;
65             }
66              
67             sub BUILD {
68 4     4 0 9995 my $self = shift;
69            
70             # init type handler:
71 4         126 $self->_Handler;
72             }
73              
74             sub index :Chained :PathPrefix {
75 18     18 0 523012 my ($self, $c, @args) = @_;
76            
77             # New: set 'abort' just like Static::Simple to suppress log messages:
78 18 50 33     640 if ( $self->no_logs && $c->log->can('abort') ) {
79 18         350 $c->log->abort( 1 );
80             }
81            
82 18         704 $self->request($c,@args);
83 0         0 $c->detach;
84 4     4   15776 }
  4         6  
  4         32  
85              
86             sub unknown_asset {
87 2     2 0 28 my ($self,$c,$asset) = @_;
88 2   33     6 $asset ||= $c->req->path;
89 2         7 $c->res->status(404);
90             # Clear any other headers that might have been set, like Etag. We don't
91             # want to allow negative caching
92 2         204 $c->res->headers->clear;
93 2         252 $c->res->header( 'Content-Type' => 'text/plain' );
94 2         349 $c->res->body( "No such asset '$asset'" );
95 2         92 return $c->detach;
96             }
97              
98             1;
99              
100             __END__
101              
102             =pod
103              
104             =head1 NAME
105              
106             Catalyst::Controller::AutoAssets - Automatic asset serving via sha1-based URLs
107              
108             =head1 SYNOPSIS
109              
110             In your controller:
111              
112             package MyApp::Controller::Assets::MyCSS;
113             use parent 'Catalyst::Controller::AutoAssets';
114            
115             1;
116              
117             Then, in your .conf:
118              
119             <Controller::Assets::MyCSS>
120             include root/my_stylesheets/
121             type CSS
122             minify 1
123             </Controller::Assets::MyCSS>
124              
125             And in your .tt files:
126              
127             <head>
128             <link rel="stylesheet" type="text/css" href="[% c.controller('Assets::MyCSS').asset_path %]" />
129             </head>
130              
131             Or, to have the appropriate tags generated for you:
132              
133             <head>
134             [% c.controller('Assets::MyCSS').html_head_tags %]
135             </head>
136              
137             Or, in static HTML:
138              
139             <head>
140             <link rel="stylesheet" type="text/css" href="/assets/mycss/current.css" />
141             </head>
142              
143             =head1 PLUGIN INTERFACE
144              
145             A Catalyst Plugin interface is also available for easy setup of multiple asset controllers at once. See
146              
147             =over
148              
149             =item L<Catalyst::Plugin::AutoAssets>
150              
151             =back
152              
153             =head1 DESCRIPTION
154              
155             Fast, convenient and extendable serving of assets (CSS, JavaScript, Images, etc) at URL path(s) containing sha1
156             checksums. This is an alternative/supplement to L<Catalyst::Plugin::Static::Simple> or
157             external/webserver for serving of an application's "nearly static" content.
158              
159             The benefit of serving files through CAS paths ("content-addressable storage" - same design used by Git)
160             is that it automatically alleviates client caching issues while simultaneously taking advantage of
161             maximum aggressive HTTP cache settings. Because URL paths contain the sha1 checksum of the data,
162             browsers can safely cache the content forever because "changes" automatically become new URLs.
163             If the content (CSS, JavaScript or other) is modified later on, the client browsers instantly
164             see the new version.
165              
166             This is particularly useful when deploying new versions of an application where client browsers
167             out in the network might have cached CSS, JavaScript and Images from previous versions. Instead of asking
168             users to hit "F5", everyone gets the new content automagically, with no intervention required (and no
169             sporadically broken user experiences when you forget to plan for cached data).
170             All you have to do is change the content; the module handles the rest.
171              
172             This module also provides some optional extra features that are useful in both development and
173             production environments for automatically managing, minifying and deploying CSS, JavaScript, Image and Icon assets.
174              
175             =head1 PERFORMANCE
176              
177             Besides the performance benefits of aggressive HTTP caching (which can be significant, depending of the
178             ratio of first-time visitors to returning visitors) this module has also been optimized to serve requests
179             as fast as possible. On typical requests, all that happens besides returning the content from
180             disk is one extra file stat and comparison of mtime. So, even with it's real-time content-change tracking and
181             checksums, this module is essentially identical to L<Catalyst::Plugin::Static::Simple> from a performance
182             perspective (and may even be slightly faster because it caches guessed Content-Types instead of calculating on every
183             request like Static::Simple).
184              
185             =head2 When to use this module
186              
187             This module is great for development, web applications, and any production site with a high percentage of
188             returning users. If you want to take maximum advantage of HTTP caching without any work or planning, this module
189             is for you. Or, if you just want an easy and flexible way to manage static content, performance benefits aside,
190             this module is also for you.
191              
192             =head2 When not to
193              
194             The only cases where this module is not recommended is on very high-volume sites where most of the visits are
195             unique (i.e. little benefit from HTTP caching), or where the scale is large enough that the marginal
196             increase in speed of serving static content directly from the web server (like Apache), instead of through Catalyst,
197             is worth manually - and correctly - doing all the things that this module does automatically. Unless you are carefully
198             planning your HTTP caching strategy (such configuring Apache's cache settings) and coordinating all this with
199             content changes/new releases, this module is likely to outperform your manual setup.
200              
201             =head1 HANDLERS
202              
203             Note: All config params and methods described below are actually delegated to the type handler specified in 'type' and some are
204             specific (as noted below). For convenience, the core handlers C<Directory>, C<CSS> and C<JS> are documented below but others
205             are available (and custom handlers can also be written). To see other available type handlers and for information on writing
206             custom handlers see:
207              
208             =over
209              
210             =item L<Catalyst::Controller::AutoAssets::Handler>
211              
212             =back
213              
214             =head1 CONFIG PARAMS
215              
216             =head2 type
217              
218             B<Required> - The asset type: C<Directory>, C<CSS>, C<JS>, etc.
219              
220             The asset type is a "Handler" class name, and the core built in types are covered below. Custom handlers
221             can also be written. See L<Catalyst::Controller::AutoAssets::Handler> for details.
222              
223             The C<Directory> asset type works in a similar manner as Static::Simple to make some directory
224             structure accessible at a public URL. The root of the structure is made available at the URL path:
225              
226             <CONTROLLER_PATH>/<SHA1>/
227              
228             L<MIME::Types> is used to set the C<Content-Type> HTTP header based on
229             the file extension (same as Static::Simple does).
230              
231             Because the sha1 checksum changes automatically and is unknown in advance, the above Asset Path is made available
232             via the C<asset_path()> controller method for use in TT files and throughout the application.
233              
234             The C<CSS> and C<JS> types serve one automatically generated text file that is concatenated and
235             optionally minified from the include files. The single, generated file is made available at the URL
236             Path:
237              
238             <CONTROLLER_PATH>/<SHA1>.js # for 'JS' type
239             <CONTROLLER_PATH>/<SHA1>.css # for 'CSS' type
240              
241             The js/css types provide a bonus mode of operation to provide a simple and convenient way to
242             manage groups of CSS and JavaScript files to be automatically deployed in the application. This
243             is also particularly useful during development. Production applications with their own management
244             and build process for CSS and JavaScript would simply use the C<Directory> type.
245              
246             =head2 no_logs
247              
248             Defaults to true to suppress log messages in the same manner as Static::Simple.
249              
250             =head2 include
251              
252             B<Required> - String or ArrayRef. The path(s) on the local filesystem containing the source asset files.
253             For C<Directory> type this must be exactly one directory, while for C<CSS> and C<JS> it can
254             be a list of directories or files. The C<include> directory becomes the root of the files hosted as-is
255             for the C<Directory> type, while for C<CSS> and C<JS> asset types it is the include files
256             concatenated together (and possibly minified) to be served as the single file.
257              
258             Source content can also be supplied directly in the form of a ScalarRef (as ScalarRef directly, or
259             included within the ArrayRef). This removes the need to have pre-existing file(s) on disk,
260             which may useful/convenient for cases involving code-generated content. This only makes sense for
261             for concatenated asset types like C<CSS> and C<JS>, since there are no filenames to reference for
262             the case of a C<Directory> asset.
263              
264             =head2 include_regex
265              
266             Optional regex ($string) to require files to match to be included.
267              
268             =head2 exclude_regex
269              
270             Optional regex ($string) to use to exclude files from the includes.
271              
272             =head2 regex_ignore_case
273              
274             Whether or not to use case-insensitive regex (qr/$regex/i vs qr/$regex/) when evaluating
275             include_regex/exclude_regex.
276              
277             Defaults to false (0).
278              
279             =head2 current_redirect
280              
281             Whether or not to make the current asset available via 307 redirect to the
282             real, current checksum/fingerprint asset path. This is a pure HTTP mechanism of resolving the
283             asset path.
284              
285             <CONTROLLER_PATH>/current/ # for 'directory' type
286             <CONTROLLER_PATH>/current.js # for 'js' type
287             <CONTROLLER_PATH>/current.css # for 'css' type
288              
289             For instance, you might reference a CSS file from a C<Directory> asset C<Controller::Assets::ExtJS>
290             using this URL path (i.e. href in an HTML C<link> tag):
291              
292             /assets/extjs/current/resources/css/ext-all.css
293              
294             This path would redirect (HTTP 307) to the current asset/file path which would be something like:
295              
296             /assets/extjs/1512834162611db1fab246dfa87e3a37f68ed95f/resources/css/ext-all.css
297              
298             The downside of this is that the server has to serve the non-cachable redirect every time, which
299             partially defeats the performance benefits of this module (although the redirect is comparatively lightweight).
300              
301             The other mechanism to find the current asset path is via the C<asset_path()> method, which returns
302             the current path outright and is the recommended usage, but is only available in locations where
303             application controller methods can be called (like in TT files).
304              
305             Defaults to true (1).
306              
307             =head2 current_alias
308              
309             Alias to use for the C<current_redirect>. Defaults to 'current' (which also implies 'current.js'/'current.css'
310             for C<JS> and C<CSS> asset types).
311              
312             =head2 allow_static_requests
313              
314             Whether or not to make the current asset available directly via a static path ('/static/'). This is like
315             current_redirect except the asset is served directly. This is essentially only useful for debug purposes
316             as it will make no use of caching.
317              
318             See also 'use_etags' below.
319              
320             Defaults to false (0).
321              
322             =head2 current_response_headers
323              
324             Extra headers to set in the response for 'current' requests. Cache-Control => 'no-cache' is always set unless
325             it is overridden here.
326              
327             Defaults to empty HashRef {}
328              
329             =head2 static_alias
330              
331             Alias to use for static requests if C<allow_static_requests> is enabled. Defaults to 'static'.
332              
333             =head2 static_response_headers
334              
335             Extra headers to set in the response for 'static' requests. Cache-Control => 'no-cache' is always set unless
336             it is overridden here.
337              
338             Defaults to empty HashRef {}
339              
340             =head2 use_etags
341              
342             Whether or not to set 'Etag' ("Entity Tag") HTTP response headers and check 'If-None-Match' client request headers to return
343             HTTP/304 'Not Modified' responses to clients that already have the current version of the requested asset/file.
344             This is essentially the same default behavior as Apache.
345              
346             Etags provide another content-based mechanism (built into HTTP 1.1) for cache validation. This module accomplishes
347             even better cache validation than Etags because it avoids the validation request needed to check the current Etag in the first place,
348             however, Etag functionality has also been included because it is very useful when enabling and using 'static' paths which
349             do not make use of the checksum in the URL. Also, when Etags are present, most browsers will use them even when hitting
350             "F5" to manually reload the page to avoid downloading the content again, so this feature further increases performance
351             for the F5 use-case which many users may be in the habit of doing for various legit reasons.
352              
353             Defaults to false (0).
354              
355             =head2 minify
356              
357             Whether or not to attempt to minify content for C<CSS> or C<JS> asset types. This is a purely optional
358             convenience feature.
359              
360             Defaults to false (0). Does not apply to the C<Directory> asset type.
361              
362             =head2 minifier
363              
364             CodeRef used to minify the content when C<minify> is true. The default code is a pass-through to
365             C<CSS::Minifier::minify()> for C<CSS> assets and C<JavaScript::Minifier::minify()> for C<JS>. If
366             you want to override you must follow the same API as in those modules, using the C<input> and
367             C<outfile> filehandle interface. See L<JavaScript::Minifier> and L<CSS::Minifier> for more details.
368              
369             Does not apply to the C<Directory> asset type.
370              
371             =head2 scopify
372              
373             Applies only to the C<CSS> asset type. CSS will be scopified using L<CSS::Scopifier>. The scopify param
374             should be an ArrayRef that will be used to pass to argument list of the C<scopify> method. Note that
375             scopify and minify are net yet supported together.
376              
377             =head2 work_dir
378              
379             The directory where asset-specific files are generated and stored. This contains the checksum/fingerprint
380             file, the lock file, and the built file. In the case of C<Directory> assets the built file contains a manifest
381             of files and in the case of C<CSS> and C<JS> assets it contains the actual asset content (concatenated and
382             possibly minified)
383              
384             Defaults to:
385              
386             <APP_TMPDIR>/AutoAssets/<CONTROLLER_PATH>/
387              
388             =head2 max_lock_wait
389              
390             Number of seconds to wait to obtain an exclusive lock when recalculating/regenerating. For thread-safety, when the system
391             needs to regenerate the asset (fingerprint and built file) it obtains an exclusive lock on the lockfile in the
392             work_dir. If another thread/process already has a lock, the system will wait for up to C<max_lock_wait> seconds
393             before proceeding anyway.
394              
395             Note that this is only relevant when the source/include content changes while the app is running (which should never
396             happen in a production environment).
397              
398             Defaults to 120 seconds.
399              
400             Also, see BUGS for caveats about locking.
401              
402             =head2 max_fingerprint_calc_age
403              
404             Max number of seconds before recalculating the fingerprint of the content (sha1 checksum)
405             regardless of whether or not the mtime has changed. 0 means infinite/disabled.
406              
407             For performance, once the system has calculated the checksum of the asset content it caches the mtime
408             of the include file(s) and verifies on each request to see if they have changed. If they have, it
409             regenerates the asset on the fly (recalculates the checksum and concatenates and minifies (if enabled)
410             for C<CSS> and C<JS> asset types). If C<max_fingerprint_calc_age> is set to a non-zero value, it will force the
411             system to regenerate at least every N seconds regardless of the mtime. This would only be needed in cases
412             where you are worried the content could change without changing the mtime which shouldn't be needed in
413             most cases.
414              
415             Defaults to 0.
416              
417             =head2 persist_state
418              
419             For faster start-up, whether or not to persist and use state data (fingerprints and mtimes) across restarts to avoid
420             rebuilding which may be expensive and unnecessary. The asset fingerprint is normally always recalculated at startup, but if this option
421             is enabled it is loaded from a cache/state file maintained on disk. This is useful for assets that take a long time
422             to build (such as big include libs) and is fine as long as you trust the state data stored on disk.
423              
424             WARNING: Use this feature with caution for 'directory' type assets since the mtime check does not catch file content changes
425             alone (only filename changes), and when this is enabled it may not catch changes even across app restarts which may
426             not be expected.
427              
428             No effect if max_fingerprint_calc_age is set.
429              
430             Defaults to false (0).
431              
432             =head2 asset_content_type
433              
434             The content type returned in the 'Content-Type' header. Defaults to C<text/css> or C<text/javascript>
435             for the C<CSS> and C<JS> types respectively.
436              
437             Does not apply to C<Directory> asset type. For files within C<Directory> type assets, the Content-Type
438             is set according to the file extension using L<MIME::Types>.
439              
440             =head2 cache_control_header
441              
442             The HTTP C<'Cache-Control'> header to return when serving assets. Defaults to the maximum
443             aggressive value that should be honored by most browsers (1 year):
444              
445             public, max-age=31536000, s-max-age=31536000
446              
447             =head2 sha1_string_length
448              
449             Optional custom length (truncated) for the SHA1 fingerprint/checksum hex string. The full 40 characters is
450             probably overkill and so this option is provided if shorter URLs are desired. The lower the number the greater
451             the chance of collision, so you just need to balance the risk with how much you want shorter URLs (not that under normal
452             use cases these URLs need to be entered by a human in the first place). If you don't understand what this means then
453             just leave this setting alone.
454              
455             Must be a integer between 5 and 40.
456              
457             Defaults to 40 (full SHA1 hex string).
458              
459             =head2 include_relative_dir
460              
461             The directory to use to resolve relative paths in the C<include> param. Defaults to the Catalyst home directory.
462              
463             =head1 METHODS
464              
465             =head2 asset_path
466              
467             Returns the current, public URL path to the asset:
468              
469             <CONTROLLER_PATH>/<SHA1> # for 'Directory' type
470             <CONTROLLER_PATH>/<SHA1>.js # for 'JS' type
471             <CONTROLLER_PATH>/<SHA1>.css # for 'CSS' type
472              
473             For C<Directory> asset types, accepts an optional subpath argument to a specific file. For example,
474             if there was a file C<images/logo.gif> within the include directory, $c->controller('Foo::MyAsset')->asset_path('images/logo.gif')
475             might return:
476              
477             /foo/myasset/1512834162611d99fab246dfa87345a37f68ed95f/images/logo.gif
478              
479             =head2 html_head_tags
480              
481             Convenience method to generate a set of tags, such as CSS <link> and JS <script>, suitable to drop
482             into the <head> section of an HTML document. What this returns, if anything, is dependent on the asset
483             type.
484              
485             =head1 BUGS/TODO
486              
487             =over
488              
489             =item Rebuilds assets on every request if they are empty (i.e. no files within the include_dir) FIXME
490              
491             =item Newly added files within a subdirectory do not trigger a rebuild and cannot be accessed, even directly,
492             because it does not change the mtime of the top directory.
493             See 'all_dirs' option below for a possible fix for this problem. The other fix would be to always check the
494             file system for an exact subfile path, even if it does not exist in the subfile_meta data.
495              
496             =item Needs an 'mtime_check_mode' option to be able to control how thorough the mtime check on every
497             request is. This mainly applies to Directory assets and could be tweaked according to the number of files.
498             Possible modes could be 'top_dir' (only check the mtime of the top directory, default for Directory),
499             'all_dirs' (check all sub directories), 'all' (check all include files, default for CSS/JS) and 'none'
500             to turn off the real-time mtime checks entirely.
501              
502             =item Needs a 'require_checksum' option to be able to require a specific asset fingerprint (such as for
503             included libs that should always have the same checksum, like the ExtJS 3.4.0 release, for instance)
504              
505             =back
506              
507             =head1 BUGS
508              
509             The AutoAssets handler uses a lock file to prevent simultaneous builds on the
510             same resource. This lock file is implemented with flock() and also setting
511             FD_CLOEXEC on the file handle, so there shouldn't be much danger of the lock
512             leaking to a child process, UNLESS your system doesn't support FD_CLOEXEC,
513             such as on Windows. So, if you're on Windows and shell out during the
514             L<build_asset> method, and your external program hangs, the lock won't get
515             released until that program is killed, regardless of whether you restart
516             your web service. (or, you can try to close all un-needed file descriptors
517             before exec()ing the external program, and avoid the problem, which is a
518             good policy anyway!)
519              
520             =head1 SEE ALSO
521              
522             =over
523              
524             =item L<Catalyst::Plugin::AutoAssets>
525              
526             =item L<Catalyst::Plugin::Assets>
527              
528             =item L<Catalyst::Controller::VersionedURI>
529              
530             =item L<Plack::Middleware::Assets>
531              
532             =item L<Plack::Middleware::JSConcat>
533              
534             =back
535              
536             =head1 AUTHOR
537              
538             Henry Van Styn <vanstyn@cpan.org>
539              
540             =head1 COPYRIGHT AND LICENSE
541              
542             This software is copyright (c) 2013 by IntelliTree Solutions llc.
543              
544             This is free software; you can redistribute it and/or modify it under
545             the same terms as the Perl 5 programming language system itself.
546              
547             =cut