Mojolicious::Static - Serve Static Files


Mojolicious documentation Contained in the Mojolicious distribution.

Index


Code Index:

NAME

Top

Mojolicious::Static - Serve Static Files

SYNOPSIS

Top

  use Mojolicious::Static;

DESCRIPTION

Top

Mojolicious::Static is a dispatcher for static files with Range and If-Modified-Since support.

ATTRIBUTES

Top

Mojolicious::Static implements the following attributes.

default_static_class

  my $class = $static->default_static_class;
  $static   = $static->default_static_class('main');

The dispatcher will use this class to look for files in the DATA section.

root

  my $root = $static->root;
  $static  = $static->root('/foo/bar/files');

Directory to serve static files from.

METHODS

Top

Mojolicious::Static inherits all methods from Mojo::Base and implements the following ones.

dispatch

  my $success = $static->dispatch($c);

Dispatch a Mojolicious::Controller object.

serve

  my $success = $static->serve($c, 'foo/bar.html');

Serve a specific file.

SEE ALSO

Top

Mojolicious, Mojolicious::Guides, http://mojolicio.us.


Mojolicious documentation Contained in the Mojolicious distribution.

package Mojolicious::Static;
use Mojo::Base -base;

use File::Basename 'dirname';
use File::stat;
use File::Spec;
use Mojo::Asset::File;
use Mojo::Asset::Memory;
use Mojo::Command;
use Mojo::Content::Single;
use Mojo::Path;

has [qw/default_static_class root/];

# "Valentine's Day's coming? Aw crap! I forgot to get a girlfriend again!"
sub dispatch {
  my ($self, $c) = @_;

  # Already rendered
  return 1 if $c->res->code;

  # Canonical path
  my $stash = $c->stash;
  my $path  = $stash->{path};
  $path = $c->req->url->path->clone->canonicalize->to_string
    unless defined $path;

  # Split parts
  my @parts = @{Mojo::Path->new->parse($path)->parts};
  return unless @parts;

  # Prevent directory traversal
  return if $parts[0] eq '..';

  # Serve static file
  if ($self->serve($c, join('/', @parts))) {
    $stash->{'mojo.static'} = 1;
    $c->rendered;
    return 1;
  }

  1;
}

sub serve {
  my ($self, $c, $rel, $root) = @_;

  # Append path to root
  $root = $self->root unless defined $root;
  my $file = File::Spec->catfile($root, split('/', $rel));

  # Extension
  $file =~ /\.(\w+)$/;
  my $ext = $1;

  # Bundled file
  $self->{_root}
    ||= File::Spec->catdir(File::Spec->splitdir(dirname(__FILE__)), 'public');
  my $bundled = File::Spec->catfile($self->{_root}, split('/', $rel));

  # Files
  my $res      = $c->res;
  my $modified = $self->{_modified} ||= time;
  my $size     = 0;
  my $asset;
  for my $path ($file, $bundled) {

    # Exists
    if (-f $path) {

      # Readable
      if (-r $path) {
        my $stat = stat($path);
        $modified = $stat->mtime;
        $size     = $stat->size;
        $asset    = Mojo::Asset::File->new(path => $path);
      }

      # Exists, but is forbidden
      else {
        $c->app->log->debug(qq/File "$rel" forbidden./);
        $res->code(403) and return 1;
      }

      # Done
      last;
    }
  }

  # DATA file
  if (!$asset && defined(my $file = $self->_get_data_file($c, $rel))) {
    $size  = length $file;
    $asset = Mojo::Asset::Memory->new->add_chunk($file);
  }

  # Found
  if ($asset) {

    # If modified since
    my $rqh = $c->req->headers;
    my $rsh = $res->headers;
    if (my $date = $rqh->if_modified_since) {

      # Not modified
      my $since = Mojo::Date->new($date)->epoch;
      if (defined $since && $since == $modified) {
        $res->code(304);
        $rsh->remove('Content-Type');
        $rsh->remove('Content-Length');
        $rsh->remove('Content-Disposition');
        return 1;
      }
    }

    # Start and end
    my $start = 0;
    my $end = $size - 1 >= 0 ? $size - 1 : 0;

    # Range
    if (my $range = $rqh->range) {
      if ($range =~ m/^bytes=(\d+)\-(\d+)?/ && $1 <= $end) {
        $start = $1;
        $end = $2 if defined $2 && $2 <= $end;
        $res->code(206);
        $rsh->content_length($end - $start + 1);
        $rsh->content_range("bytes $start-$end/$size");
      }
      else {

        # Not satisfiable
        $res->code(416);
        return 1;
      }
    }
    $asset->start_range($start);
    $asset->end_range($end);

    # Prepare response
    $res->code(200) unless $res->code;
    $res->content->asset($asset);
    $rsh->content_type($c->app->types->type($ext) || 'text/plain');
    $rsh->accept_ranges('bytes');
    $rsh->last_modified(Mojo::Date->new($modified));
    return 1;
  }

  undef;
}

sub _get_data_file {
  my ($self, $c, $rel) = @_;

  # Protect templates
  return if $rel =~ /\.\w+\.\w+$/;

  # Detect DATA class
  my $class =
       $c->stash->{static_class}
    || $ENV{MOJO_STATIC_CLASS}
    || $self->default_static_class
    || 'main';

  # Find DATA file
  my $data = $self->{_data_files}->{$class}
    ||= [keys %{Mojo::Command->new->get_all_data($class) || {}}];
  for my $path (@$data) {
    return Mojo::Command->new->get_data($path, $class) if $path eq $rel;
  }

  undef;
}

1;
__END__