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 3     3   707954 use strict;
  3         5  
  3         86  
3 3     3   13 use warnings;
  3         4  
  3         119  
4              
5             our $VERSION = 0.32;
6              
7 3     3   20 use Moose;
  3         4  
  3         24  
8 3     3   16742 use namespace::autoclean;
  3         5  
  3         25  
9             require Module::Runtime;
10              
11 3     3   230 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 3     3   6 my $self = shift;
54 3         5 my $class = shift;
55            
56             # legacy, original, lower-case, built-in type names:
57 3         10 my %type_aliases = ( css => 'CSS', js => 'JS', directory => 'Directory' );
58 3 50       12 $class = $type_aliases{$class} if (exists $type_aliases{$class});
59            
60             # Allow absolute class names using '+' prefix:
61 3 50       16 $class = $class =~ /^\+(.*)$/ ? $1
62             : "Catalyst::Controller::AutoAssets::Handler::$class";
63 3         15 Module::Runtime::require_module($class);
64 3         19 return $class;
65             }
66              
67             sub BUILD {
68 3     3 0 7651 my $self = shift;
69            
70             # init type handler:
71 3         111 $self->_Handler;
72             }
73              
74             sub index :Chained :PathPrefix {
75 16     16 0 392617 my ($self, $c, @args) = @_;
76            
77             # New: set 'abort' just like Static::Simple to suppress log messages:
78 16 50 33     433 if ( $self->no_logs && $c->log->can('abort') ) {
79 16         307 $c->log->abort( 1 );
80             }
81            
82 16         609 $self->request($c,@args);
83 0         0 $c->detach;
84 3     3   14281 }
  3         6  
  3         26  
85              
86             sub unknown_asset {
87 2     2 0 35 my ($self,$c,$asset) = @_;
88 2   33     6 $asset ||= $c->req->path;
89 2         8 $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         211 $c->res->headers->clear;
93 2         245 $c->res->header( 'Content-Type' => 'text/plain' );
94 2         334 $c->res->body( "No such asset '$asset'" );
95 2         93 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. 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             =head2 include_regex
259              
260             Optional regex ($string) to require files to match to be included.
261              
262             =head2 exclude_regex
263              
264             Optional regex ($string) to use to exclude files from the includes.
265              
266             =head2 regex_ignore_case
267              
268             Whether or not to use case-insensitive regex (qr/$regex/i vs qr/$regex/) when evaluating
269             include_regex/exclude_regex.
270              
271             Defaults to false (0).
272              
273             =head2 current_redirect
274              
275             Whether or not to make the current asset available via 307 redirect to the
276             real, current checksum/fingerprint asset path. This is a pure HTTP mechanism of resolving the
277             asset path.
278              
279             <CONTROLLER_PATH>/current/ # for 'directory' type
280             <CONTROLLER_PATH>/current.js # for 'js' type
281             <CONTROLLER_PATH>/current.css # for 'css' type
282              
283             For instance, you might reference a CSS file from a C<Directory> asset C<Controller::Assets::ExtJS>
284             using this URL path (i.e. href in an HTML C<link> tag):
285              
286             /assets/extjs/current/resources/css/ext-all.css
287              
288             This path would redirect (HTTP 307) to the current asset/file path which would be something like:
289              
290             /assets/extjs/1512834162611db1fab246dfa87e3a37f68ed95f/resources/css/ext-all.css
291              
292             The downside of this is that the server has to serve the non-cachable redirect every time, which
293             partially defeats the performance benefits of this module (although the redirect is comparatively lightweight).
294              
295             The other mechanism to find the current asset path is via the C<asset_path()> method, which returns
296             the current path outright and is the recommended usage, but is only available in locations where
297             application controller methods can be called (like in TT files).
298              
299             Defaults to true (1).
300              
301             =head2 current_alias
302              
303             Alias to use for the C<current_redirect>. Defaults to 'current' (which also implies 'current.js'/'current.css'
304             for C<JS> and C<CSS> asset types).
305              
306             =head2 allow_static_requests
307              
308             Whether or not to make the current asset available directly via a static path ('/static/'). This is like
309             current_redirect except the asset is served directly. This is essentially only useful for debug purposes
310             as it will make no use of caching.
311              
312             See also 'use_etags' below.
313              
314             Defaults to false (0).
315              
316             =head2 current_response_headers
317              
318             Extra headers to set in the response for 'current' requests. Cache-Control => 'no-cache' is always set unless
319             it is overridden here.
320              
321             Defaults to empty HashRef {}
322              
323             =head2 static_alias
324              
325             Alias to use for static requests if C<allow_static_requests> is enabled. Defaults to 'static'.
326              
327             =head2 static_response_headers
328              
329             Extra headers to set in the response for 'static' requests. Cache-Control => 'no-cache' is always set unless
330             it is overridden here.
331              
332             Defaults to empty HashRef {}
333              
334             =head2 use_etags
335              
336             Whether or not to set 'Etag' ("Entity Tag") HTTP response headers and check 'If-None-Match' client request headers to return
337             HTTP/304 'Not Modified' responses to clients that already have the current version of the requested asset/file.
338             This is essentially the same default behavior as Apache.
339              
340             Etags provide another content-based mechanism (built into HTTP 1.1) for cache validation. This module accomplishes
341             even better cache validation than Etags because it avoids the validation request needed to check the current Etag in the first place,
342             however, Etag functionality has also been included because it is very useful when enabling and using 'static' paths which
343             do not make use of the checksum in the URL. Also, when Etags are present, most browsers will use them even when hitting
344             "F5" to manually reload the page to avoid downloading the content again, so this feature further increases performance
345             for the F5 use-case which many users may be in the habit of doing for various legit reasons.
346              
347             Defaults to false (0).
348              
349             =head2 minify
350              
351             Whether or not to attempt to minify content for C<CSS> or C<JS> asset types. This is a purely optional
352             convenience feature.
353              
354             Defaults to false (0). Does not apply to the C<Directory> asset type.
355              
356             =head2 minifier
357              
358             CodeRef used to minify the content when C<minify> is true. The default code is a pass-through to
359             C<CSS::Minifier::minify()> for C<CSS> assets and C<JavaScript::Minifier::minify()> for C<JS>. If
360             you want to override you must follow the same API as in those modules, using the C<input> and
361             C<outfile> filehandle interface. See L<JavaScript::Minifier> and L<CSS::Minifier> for more details.
362              
363             Does not apply to the C<Directory> asset type.
364              
365             =head2 scopify
366              
367             Applies only to the C<CSS> asset type. CSS will be scopified using L<CSS::Scopifier>. The scopify param
368             should be an ArrayRef that will be used to pass to argument list of the C<scopify> method. Note that
369             scopify and minify are net yet supported together.
370              
371             =head2 work_dir
372              
373             The directory where asset-specific files are generated and stored. This contains the checksum/fingerprint
374             file, the lock file, and the built file. In the case of C<Directory> assets the built file contains a manifest
375             of files and in the case of C<CSS> and C<JS> assets it contains the actual asset content (concatenated and
376             possibly minified)
377              
378             Defaults to:
379              
380             <APP_TMPDIR>/AutoAssets/<CONTROLLER_PATH>/
381              
382             =head2 max_lock_wait
383              
384             Number of seconds to wait to obtain an exclusive lock when recalculating/regenerating. For thread-safety, when the system
385             needs to regenerate the asset (fingerprint and built file) it obtains an exclusive lock on the lockfile in the
386             work_dir. If another thread/process already has a lock, the system will wait for up to C<max_lock_wait> seconds
387             before proceeding anyway.
388              
389             Note that this is only relevant when the source/include content changes while the app is running (which should never
390             happen in a production environment).
391              
392             Defaults to 120 seconds.
393              
394             Also, see BUGS for caveats about locking.
395              
396             =head2 max_fingerprint_calc_age
397              
398             Max number of seconds before recalculating the fingerprint of the content (sha1 checksum)
399             regardless of whether or not the mtime has changed. 0 means infinite/disabled.
400              
401             For performance, once the system has calculated the checksum of the asset content it caches the mtime
402             of the include file(s) and verifies on each request to see if they have changed. If they have, it
403             regenerates the asset on the fly (recalculates the checksum and concatenates and minifies (if enabled)
404             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
405             system to regenerate at least every N seconds regardless of the mtime. This would only be needed in cases
406             where you are worried the content could change without changing the mtime which shouldn't be needed in
407             most cases.
408              
409             Defaults to 0.
410              
411             =head2 persist_state
412              
413             For faster start-up, whether or not to persist and use state data (fingerprints and mtimes) across restarts to avoid
414             rebuilding which may be expensive and unnecessary. The asset fingerprint is normally always recalculated at startup, but if this option
415             is enabled it is loaded from a cache/state file maintained on disk. This is useful for assets that take a long time
416             to build (such as big include libs) and is fine as long as you trust the state data stored on disk.
417              
418             WARNING: Use this feature with caution for 'directory' type assets since the mtime check does not catch file content changes
419             alone (only filename changes), and when this is enabled it may not catch changes even across app restarts which may
420             not be expected.
421              
422             No effect if max_fingerprint_calc_age is set.
423              
424             Defaults to false (0).
425              
426             =head2 asset_content_type
427              
428             The content type returned in the 'Content-Type' header. Defaults to C<text/css> or C<text/javascript>
429             for the C<CSS> and C<JS> types respectively.
430              
431             Does not apply to C<Directory> asset type. For files within C<Directory> type assets, the Content-Type
432             is set according to the file extension using L<MIME::Types>.
433              
434             =head2 cache_control_header
435              
436             The HTTP C<'Cache-Control'> header to return when serving assets. Defaults to the maximum
437             aggressive value that should be honored by most browsers (1 year):
438              
439             public, max-age=31536000, s-max-age=31536000
440              
441             =head2 sha1_string_length
442              
443             Optional custom length (truncated) for the SHA1 fingerprint/checksum hex string. The full 40 characters is
444             probably overkill and so this option is provided if shorter URLs are desired. The lower the number the greater
445             the chance of collision, so you just need to balance the risk with how much you want shorter URLs (not that under normal
446             use cases these URLs need to be entered by a human in the first place). If you don't understand what this means then
447             just leave this setting alone.
448              
449             Must be a integer between 5 and 40.
450              
451             Defaults to 40 (full SHA1 hex string).
452              
453             =head2 include_relative_dir
454              
455             The directory to use to resolve relative paths in the C<include> param. Defaults to the Catalyst home directory.
456              
457             =head1 METHODS
458              
459             =head2 asset_path
460              
461             Returns the current, public URL path to the asset:
462              
463             <CONTROLLER_PATH>/<SHA1> # for 'Directory' type
464             <CONTROLLER_PATH>/<SHA1>.js # for 'JS' type
465             <CONTROLLER_PATH>/<SHA1>.css # for 'CSS' type
466              
467             For C<Directory> asset types, accepts an optional subpath argument to a specific file. For example,
468             if there was a file C<images/logo.gif> within the include directory, $c->controller('Foo::MyAsset')->asset_path('images/logo.gif')
469             might return:
470              
471             /foo/myasset/1512834162611d99fab246dfa87345a37f68ed95f/images/logo.gif
472              
473             =head2 html_head_tags
474              
475             Convenience method to generate a set of tags, such as CSS <link> and JS <script>, suitable to drop
476             into the <head> section of an HTML document. What this returns, if anything, is dependent on the asset
477             type.
478              
479             =head1 BUGS/TODO
480              
481             =over
482              
483             =item Rebuilds assets on every request if they are empty (i.e. no files within the include_dir) FIXME
484              
485             =item Newly added files within a subdirectory do not trigger a rebuild and cannot be accessed, even directly,
486             because it does not change the mtime of the top directory.
487             See 'all_dirs' option below for a possible fix for this problem. The other fix would be to always check the
488             file system for an exact subfile path, even if it does not exist in the subfile_meta data.
489              
490             =item Needs an 'mtime_check_mode' option to be able to control how thorough the mtime check on every
491             request is. This mainly applies to Directory assets and could be tweaked according to the number of files.
492             Possible modes could be 'top_dir' (only check the mtime of the top directory, default for Directory),
493             'all_dirs' (check all sub directories), 'all' (check all include files, default for CSS/JS) and 'none'
494             to turn off the real-time mtime checks entirely.
495              
496             =item Needs a 'require_checksum' option to be able to require a specific asset fingerprint (such as for
497             included libs that should always have the same checksum, like the ExtJS 3.4.0 release, for instance)
498              
499             =back
500              
501             =head1 BUGS
502              
503             The AutoAssets handler uses a lock file to prevent simultaneous builds on the
504             same resource. This lock file is implemented with flock() and also setting
505             FD_CLOEXEC on the file handle, so there shouldn't be much danger of the lock
506             leaking to a child process, UNLESS your system doesn't support FD_CLOEXEC,
507             such as on Windows. So, if you're on Windows and shell out during the
508             L<build_asset> method, and your external program hangs, the lock won't get
509             released until that program is killed, regardless of whether you restart
510             your web service. (or, you can try to close all un-needed file descriptors
511             before exec()ing the external program, and avoid the problem, which is a
512             good policy anyway!)
513              
514             =head1 SEE ALSO
515              
516             =over
517              
518             =item L<Catalyst::Plugin::AutoAssets>
519              
520             =item L<Catalyst::Plugin::Assets>
521              
522             =item L<Catalyst::Controller::VersionedURI>
523              
524             =item L<Plack::Middleware::Assets>
525              
526             =item L<Plack::Middleware::JSConcat>
527              
528             =back
529              
530             =head1 AUTHOR
531              
532             Henry Van Styn <vanstyn@cpan.org>
533              
534             =head1 COPYRIGHT AND LICENSE
535              
536             This software is copyright (c) 2013 by IntelliTree Solutions llc.
537              
538             This is free software; you can redistribute it and/or modify it under
539             the same terms as the Perl 5 programming language system itself.
540              
541             =cut