Skip to content

jjn1056/Catalyst-ActionRole-Public

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

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.

About

Map Public Urls to files on your filesystem

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages