The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

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) {  }

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

Will create an action that from URL 'https://myhost.com/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 such as a 'Not Found' catchall.

It also builds in support for Plack::Middleware::XSendfile by using "set_io_path" in Plack::Util on the filehandle object. This way can can take advantage of static file serving via a fast webserver even if you need to serve such files from behind a password protected route.

If you prefer to use Chaining, here's the same type of match:

    package MyApp::Controller::Root;

    use Moose;
    use MooseX::MethodAttributes;

    extends  'Catalyst::Controller';

    sub root :Chained(/) PathPart('') CaptureArgs(0) { }

    sub static :Chained(root) Args Does(Public) { }

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

This would have the effect of making the directory at "$c->config->root . '/static'" served as a public directory.

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...)

Why not use Catalyst::Plugin::Static::Simple?

The only upsides to this action role is 1) it supports Plack::Middleware::XSendfile, 2) it gives you an action to target for static files, using uri_for and 3) its a more targeted level of composition rather than a plugin which you mighty just happen to like more.

The plugin can do somethings this can't (yet) such as define multiple root paths for finding your static files (or even have a function provide the path dynamically).

If the classic plugin works for you and you are happy there's little reason to use this.

Also this is not a bad example code for creating actions roles.

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 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.

At

Serves

Used to set the action role template used to match files on the filesystem to incoming requests. Either 'At' or 'Serves' can be used to avoid namespace collision with other actions roles. 'Serves' will be looked at first in the case where more than one exists. Useful if you have complex needs or to match a single file:

    package MyApp::Controller::Root;

    use Moose;
    use MooseX::MethodAttributes;

    extends  'Catalyst::Controller';

    welcome :Path('welcome.txt') Args(0) Does(Public) Serves('/welcome.txt')  { }

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

Will just match 'https://localhost/welcome.txt'. Although it seems verbose we are cleanly distinguishing between the URL that is matched, the private name of the matched action, and the actual file path served. You might do something like this if you have a static file you want to serve behind a password. Here's a similar type of pattern if you are using Chaining:

    package MyApp::Controller::Root;

    use Moose;
    use MooseX::MethodAttributes;

    extends  'Catalyst::Controller';

    sub root :Chained(/) PathPart('') CaptureArgs(0) { }

    sub one_file :Chained(root) 
      PathPart('one.txt') Args(0)
      Does(Public) Serves('/one.txt') { }

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

Please note the '/' in "Serves('/one.txt')". This would math the URL 'https://hostname/one.txt' and serve the file:

    $c->config->{root} . '/one.txt'

If you left off the '/' the file path would be relative to the action private name (in this case $c->config->{root} . 'one_file' . '/one.txt').

More Examples:

    package MyApp::Controller::Basic;

    use Moose;
    use MooseX::MethodAttributes;

    extends  'Catalyst::Controller';

    #localhost/basic/welcome
    sub welcome :Path('welcome.txt') Args(0) Does(Public) { }

    #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) { ...}

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.

NOTE Although you can set this on the action controller code, it is likely you will prefer to set it via configuration (for example turn it on only in development). For example:

    __PACKAGE__->config(
      'Controller::Root' => {
        actions => {
          myaction => { ShowDebugging => },
        },
      },
    );

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.