NAME

Catalyst::ActionRole::Public - Mount a public url to files in your project directory.

SYNOPSIS

package MyApp::Controller::Root;

use Moose;
use MooseX::MethodAttributes;

sub static :Local Does(Public) At(/:actionname/*) { ... }

__PACKAGE__->config(namespace=>'');

Will create an action that from URL 'localhost/static/a/b/c/d.js' will serve file $c->config->{root} . '/static' . '/a/b/c/d.js'. Will also set content type, length and Last-Modified HTTP headers as needed. If the file does not exist, will not match (allowing possibly other actions to match).

DESCRIPTION

Use this actionrole to map a public facing URL attached to an action to a file (or files) on the filesystem, off the $c->config->{root} directory. If the file does not exist, the action will not match. No default 'notfound' page is created, unlike Plack::App::File or Catalyst::Plugin::Static::Simple. The action method body may be used to modify the response before finalization.

A template may be constructed to determine how we map an incoming request to a path on the filesystem. You have extensive control how an incoming HTTP request maps to a file on the filesystem. You can even use this action role in the middle of a chained style action (although its hard to imagine the use case for that...)

ACTION METHOD BODY

The body of your action will be executed after we've created a filehandle to the found file and setup the response. You may leave it empty, or if you want to do additional logging or work, you can. Also, you will find a stash key public_file_path has been populated with a Path::Class object which is pointing to the found file. The action method body will not be executed if the file associated with the action does not exist.

ACTION ATTRIBUTES

Actions the consume this role provide the following subroutine attributes.

ShowDebugging

Enabled developer debugging output. Example:

sub myaction :Local Does(Public) ShowDebugging { ... }

If present do not surpress the extra developer mode debugging information. Useful if you have trouble serving files and you can't figure out why.

At

Used to set the action class template used to match files on the filesystem to incoming requests. Examples:

package MyApp::Controller::Basic;

use Moose;
use MooseX::MethodAttributes;

extends  'Catalyst::Controller';

#localhost/basic/css => $c->config->{root} .'/basic/*'
sub css :Local Does(Public) At(/:namespace/*) { }

#localhost/basic/static => $c->config->{root} .'/basic/static/*'
sub static :Local Does(Public) { }

#localhost/basic/111/aaa/link2/333/444.txt => $c->config->{root} .'/basic/link2/333/444.txt'
sub chainbase :Chained(/) PathPrefix CaptureArgs(1) { }

  sub link1 :Chained(chainbase) PathPart(aaa) CaptureArgs(0) { }

    sub link2 :Chained(link1) Args(2) Does(Public) { }

#localhost/chainbase2/111/aaa/222.txt/link4/333 => $c->config->{root} . '/basic/link3/222.txt'
sub chainbase2 :Chained(/)  CaptureArgs(1) { }

  sub link3 :Chained(chainbase2) PathPart(aaa) CaptureArgs(1) Does(Public) { }

    sub link4 :Chained(link3) Args(1)  { }

1;

NOTE: You're template may be 'relative or absolute' to the $c->config->{root} value based on if the first character in the template is '/' or not. If it is '/' that is an 'absolute' template which will be added to $c->config->{root}. Generally if you are making a template this is what you want. However if you don't have a '/' prepended to the start of your template (such as in At(file.txt)) we then make your filesystem lookup relative to the action private path. So in the example:

package MyApp::Controller::Basic;

sub absolute_path :Path('/example1') Does(Public) At(/example.txt) { }
sub relative_path :Path('/example2') Does(Public) At(example.txt) { }

Then http://localhost/example1 => $c->config->{root} . '/example.txt' but http://localhost/example2 => $c->config->{root} . '/basic/relative_path/example.txt'. You may find this a useful "DWIW" when an action is linked to a particular file.

NOTE: The following expansions are recognized in your At declaration:

:namespace

The action namespace, determined from the containing controller. Usually this is based on the Controller package name but you can override it via controller configuration. For example:

package MyApp::Controller::Foo::Bar::Baz;

Has a namespace of 'foo/bar/baz' by default.

:privatepath
:private_path

The action private_path value. By default this is the namespace + the action name. For example:

package MyApp::Controller::Foo::Bar::Baz;

sub myaction :Path('abcd') { ... }

The action myaction has a private_path of '/foo/bar/baz/myaction'.

NOTE: the expansion :private_path is mapped to this value as well.

actionname
action_name

The name of the action (typically the subroutine name)

sub static :Local Does(Public) At(/:actionname/*) { ... }

In this case actionname = 'static'

:args
'*'

The arguments to the request. For example:

Package MyApp::Controller::Static;

sub myfiles :Path('') Does(Public) At(/:namespace/*) { ... }

Would map 'http://localhost/static/a/b/c/d.txt' to $c->config->{root} . '/static/a/b/c/d.txt'.

In this case $args = ['a', 'b', 'c', 'd.txt']

ContentType

Used to set the response Content-Type header and match the file extension. Example:

sub myaction :Local Does(Public) ContentType(application/javascript) { ... }

By default we inspect the request URL extension and set a content type based on the extension text (defaulting to 'application/octet' if we cannot determine). If you set this to a MIME type, we will always set the response content type based on this. Also, we will only match static files on the filesystem whose extensions match the declared type.

You may declare more than one ContentType, in which case all allowed types are permitted in the match.

CacheControl

Used to set the Cache-Control HTTP header (useful for caching your static assets). Example:

sub myaction :Local Does(Public) CacheControl(public, max-age=600) { ...}

RESPONSE INFO

If we find a file we serve the filehandle directly to you plack handler, and set a 'with_path' value so that you can use this with something like Plack::Middleware::XSendfile. We also set the Content-Type, Content-Length and Last-Modified headers. If you need to add more information before finalizing the response you may do so with the matching action metod body.

COOKBOOK

I often use this in a Root.pm controller like:

package MyApp::Web::Controller::Root;

use Moose;
use MooseX::MethodAttributes;
use HTTP::Exception;

extends 'Catalyst::Controller';

sub root :Chained(/) PathPart('') CaptureArgs(0) {
  my ($self, $c) = @_; 
}

  sub index :Chained(root) PathPart('') Args(0) {
    my ($self, $c) = @_;
  }

  sub css :Chained(root) Args Does(Public) ContentType(text/css) { } 
  sub js  :Chained(root) Args Does(Public) ContentType(application/javascript) { } 
  sub img :Chained(root) Args Does(Public) { }
  sub html :Chained(root) PathPart('') Args Does(Public) At(/:args) ContentType(text/html,text/plain) { }

sub default :Default { HTTP::Exception->throw(404) }
sub end :ActionClass(RenderView) { }

__PACKAGE__->config(namespace=>'');
__PACKAGE__->meta->make_immutable;

This sets up to let me mix my templates and static files under $c->config->{root} and in general prevents non asset types from being accidentally posted. I might then have a directory of files like:

root/
  css/
  js/
  img/
  index.html
  welcome.template

FWIW!

AUTHOR

John Napiorkowski email:jjnapiork@cpan.org

SEE ALSO

Catalyst, Catalyst::Controller, Plack::App::Directory, Catalyst::Controller::Assets.

COPYRIGHT & LICENSE

Copyright 2015, John Napiorkowski email:jjnapiork@cpan.org

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.