—package
Mojolicious::Static;
use
Mojo::Base -base;
use
Mojo::Asset::File;
use
Mojo::Asset::Memory;
use
Mojo::Date;
use
Mojo::Home;
use
Mojo::Loader;
has
classes
=>
sub
{ [
'main'
] };
has
paths
=>
sub
{ [] };
# Last modified default
my
$MTIME
=
time
;
# Bundled files
my
$HOME
= Mojo::Home->new;
my
$PUBLIC
=
$HOME
->parse(
$HOME
->mojo_lib_dir)->rel_dir(
'Mojolicious/public'
);
sub
dispatch {
my
(
$self
,
$c
) =
@_
;
# Canonical path
my
$stash
=
$c
->stash;
my
$path
=
$c
->req->url->path;
$path
=
$stash
->{path} ?
$path
->new(
$stash
->{path}) :
$path
->clone;
return
undef
unless
my
@parts
= @{
$path
->canonicalize->parts};
# Serve static file and prevent directory traversal
return
undef
if
$parts
[0] eq
'..'
|| !
$self
->serve(
$c
,
join
(
'/'
,
@parts
));
$stash
->{
'mojo.static'
}++;
return
!!
$c
->rendered;
}
sub
file {
my
(
$self
,
$rel
) =
@_
;
# Search all paths
for
my
$path
(@{
$self
->paths}) {
next
unless
my
$asset
=
$self
->_get_file(catfile
$path
,
split
(
'/'
,
$rel
));
return
$asset
;
}
# Search DATA
if
(
my
$asset
=
$self
->_get_data_file(
$rel
)) {
return
$asset
}
# Search bundled files
return
$self
->_get_file(catfile(
$PUBLIC
,
split
(
'/'
,
$rel
)));
}
sub
serve {
my
(
$self
,
$c
,
$rel
) =
@_
;
return
undef
unless
my
$asset
=
$self
->file(
$rel
);
my
$types
=
$c
->app->types;
my
$type
=
$rel
=~ /\.(\w+)$/ ?
$types
->type($1) :
undef
;
$c
->res->headers->content_type(
$type
||
$types
->type(
'txt'
));
return
!!
$self
->serve_asset(
$c
,
$asset
);
}
sub
serve_asset {
my
(
$self
,
$c
,
$asset
) =
@_
;
# Last modified
my
$mtime
=
$asset
->is_file ? (
stat
$asset
->path)[9] :
$MTIME
;
my
$res
=
$c
->res;
$res
->code(200)->headers->last_modified(Mojo::Date->new(
$mtime
))
->accept_ranges(
'bytes'
);
# If modified since
my
$headers
=
$c
->req->headers;
if
(
my
$date
=
$headers
->if_modified_since) {
my
$since
= Mojo::Date->new(
$date
)->epoch;
return
$res
->code(304)
if
defined
$since
&&
$since
==
$mtime
;
}
# Range
my
$size
=
$asset
->size;
my
$start
= 0;
my
$end
=
$size
- 1;
if
(
my
$range
=
$headers
->range) {
# Not satisfiable
return
$res
->code(416)
unless
$size
&&
$range
=~ m/^bytes=(\d+)?-(\d+)?/;
$start
= $1
if
defined
$1;
$end
= $2
if
defined
$2 && $2 <=
$end
;
return
$res
->code(416)
if
$start
>
$end
||
$end
> (
$size
- 1);
# Satisfiable
$res
->code(206)->headers->content_length(
$end
-
$start
+ 1)
->content_range(
"bytes $start-$end/$size"
);
}
return
$res
->content->asset(
$asset
->start_range(
$start
)->end_range(
$end
));
}
sub
_get_data_file {
my
(
$self
,
$rel
) =
@_
;
# Protect templates
return
undef
if
$rel
=~ /\.\w+\.\w+$/;
# Index DATA files
my
$loader
= Mojo::Loader->new;
unless
(
$self
->{
index
}) {
my
$index
=
$self
->{
index
} = {};
for
my
$class
(
reverse
@{
$self
->classes}) {
$index
->{
$_
} =
$class
for
keys
%{
$loader
->data(
$class
)};
}
}
# Find file
return
undef
unless
defined
(
my
$data
=
$loader
->data(
$self
->{
index
}{
$rel
},
$rel
));
return
Mojo::Asset::Memory->new->add_chunk(
$data
);
}
sub
_get_file {
my
(
$self
,
$path
) =
@_
;
no
warnings
'newline'
;
return
-f
$path
&& -r
$path
? Mojo::Asset::File->new(
path
=>
$path
) :
undef
;
}
1;
=encoding utf8
=head1 NAME
Mojolicious::Static - Serve static files
=head1 SYNOPSIS
use Mojolicious::Static;
my $static = Mojolicious::Static->new;
push @{$static->classes}, 'MyApp::Foo';
push @{$static->paths}, '/home/sri/public';
=head1 DESCRIPTION
L<Mojolicious::Static> is a static file server with C<Range> and
C<If-Modified-Since> support.
=head1 ATTRIBUTES
L<Mojolicious::Static> implements the following attributes.
=head2 classes
my $classes = $static->classes;
$static = $static->classes(['main']);
Classes to use for finding files in C<DATA> sections, first one has the
highest precedence, defaults to C<main>.
# Add another class with static files in DATA section
push @{$static->classes}, 'Mojolicious::Plugin::Fun';
=head2 paths
my $paths = $static->paths;
$static = $static->paths(['/home/sri/public']);
Directories to serve static files from, first one has the highest precedence.
# Add another "public" directory
push @{$static->paths}, '/home/sri/public';
=head1 METHODS
L<Mojolicious::Static> inherits all methods from L<Mojo::Base> and implements
the following new ones.
=head2 dispatch
my $bool = $static->dispatch(Mojolicious::Controller->new);
Serve static file for L<Mojolicious::Controller> object.
=head2 file
my $asset = $static->file('images/logo.png');
my $asset = $static->file('../lib/MyApp.pm');
Build L<Mojo::Asset::File> or L<Mojo::Asset::Memory> object for a file,
relative to L</"paths"> or from L</"classes">. Note that this method does not
protect from traversing to parent directories.
my $content = $static->file('foo/bar.html')->slurp;
=head2 serve
my $bool = $static->serve(Mojolicious::Controller->new, 'images/logo.png');
my $bool = $static->serve(Mojolicious::Controller->new, '../lib/MyApp.pm');
Serve a specific file, relative to L</"paths"> or from L</"classes">. Note
that this method does not protect from traversing to parent directories.
=head2 serve_asset
$static->serve_asset(Mojolicious::Controller->new, Mojo::Asset::File->new);
Serve a L<Mojo::Asset::File> or L<Mojo::Asset::Memory> object with C<Range>
and C<If-Modified-Since> support.
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>.
=cut