add files
|
1 |
package Mojolicious::Static; |
2 |
use Mojo::Base -base; |
|
3 | ||
4 |
use File::Spec::Functions 'catfile'; |
|
5 |
use Mojo::Asset::File; |
|
6 |
use Mojo::Asset::Memory; |
|
7 |
use Mojo::Home; |
|
8 |
use Mojo::Loader; |
|
9 | ||
10 |
has classes => sub { ['main'] }; |
|
11 |
has paths => sub { [] }; |
|
12 | ||
13 |
# Last modified default |
|
14 |
my $MTIME = time; |
|
15 | ||
16 |
# Bundled files |
|
17 |
my $HOME = Mojo::Home->new; |
|
18 |
my $PUBLIC = $HOME->parse($HOME->mojo_lib_dir)->rel_dir('Mojolicious/public'); |
|
19 | ||
20 |
sub dispatch { |
|
21 |
my ($self, $c) = @_; |
|
22 | ||
23 |
# Canonical path |
|
24 |
my $stash = $c->stash; |
|
25 |
my $path = $c->req->url->path; |
|
26 |
$path = $stash->{path} ? $path->new($stash->{path}) : $path->clone; |
|
27 |
return undef unless my @parts = @{$path->canonicalize->parts}; |
|
28 | ||
29 |
# Serve static file and prevent directory traversal |
|
30 |
return undef if $parts[0] eq '..' || !$self->serve($c, join('/', @parts)); |
|
31 |
$stash->{'mojo.static'}++; |
|
32 |
return !!$c->rendered; |
|
33 |
} |
|
34 | ||
35 |
sub file { |
|
36 |
my ($self, $rel) = @_; |
|
37 | ||
38 |
# Search all paths |
|
39 |
for my $path (@{$self->paths}) { |
|
40 |
next unless my $asset = $self->_get_file(catfile $path, split('/', $rel)); |
|
41 |
return $asset; |
|
42 |
} |
|
43 | ||
44 |
# Search DATA |
|
45 |
if (my $asset = $self->_get_data_file($rel)) { return $asset } |
|
46 | ||
47 |
# Search bundled files |
|
48 |
return $self->_get_file(catfile($PUBLIC, split('/', $rel))); |
|
49 |
} |
|
50 | ||
51 |
sub serve { |
|
52 |
my ($self, $c, $rel) = @_; |
|
53 |
return undef unless my $asset = $self->file($rel); |
|
54 |
my $type = $rel =~ /\.(\w+)$/ ? $c->app->types->type($1) : undef; |
|
55 |
$c->res->headers->content_type($type || 'text/plain'); |
|
56 |
return !!$self->serve_asset($c, $asset); |
|
57 |
} |
|
58 | ||
59 |
sub serve_asset { |
|
60 |
my ($self, $c, $asset) = @_; |
|
61 | ||
62 |
# Last modified |
|
63 |
my $mtime = $asset->is_file ? (stat $asset->path)[9] : $MTIME; |
|
64 |
my $res = $c->res; |
|
65 |
$res->code(200)->headers->last_modified(Mojo::Date->new($mtime)) |
|
66 |
->accept_ranges('bytes'); |
|
67 | ||
68 |
# If modified since |
|
69 |
my $headers = $c->req->headers; |
|
70 |
if (my $date = $headers->if_modified_since) { |
|
71 |
my $since = Mojo::Date->new($date)->epoch; |
|
72 |
return $res->code(304) if defined $since && $since == $mtime; |
|
73 |
} |
|
74 | ||
75 |
# Range |
|
76 |
my $size = $asset->size; |
|
77 |
my $start = 0; |
|
78 |
my $end = $size - 1; |
|
79 |
if (my $range = $headers->range) { |
|
80 | ||
81 |
# Not satisfiable |
|
82 |
return $res->code(416) unless $size && $range =~ m/^bytes=(\d+)?-(\d+)?/; |
|
83 |
$start = $1 if defined $1; |
|
84 |
$end = $2 if defined $2 && $2 <= $end; |
|
85 |
return $res->code(416) if $start > $end || $end > ($size - 1); |
|
86 | ||
87 |
# Satisfiable |
|
88 |
$res->code(206)->headers->content_length($end - $start + 1) |
|
89 |
->content_range("bytes $start-$end/$size"); |
|
90 |
} |
|
91 | ||
92 |
return $res->content->asset($asset->start_range($start)->end_range($end)); |
|
93 |
} |
|
94 | ||
95 |
sub _get_data_file { |
|
96 |
my ($self, $rel) = @_; |
|
97 | ||
98 |
# Protect templates |
|
99 |
return undef if $rel =~ /\.\w+\.\w+$/; |
|
100 | ||
101 |
# Index DATA files |
|
102 |
my $loader = Mojo::Loader->new; |
|
103 |
unless ($self->{index}) { |
|
104 |
my $index = $self->{index} = {}; |
|
105 |
for my $class (reverse @{$self->classes}) { |
|
106 |
$index->{$_} = $class for keys %{$loader->data($class)}; |
|
107 |
} |
|
108 |
} |
|
109 | ||
110 |
# Find file |
|
111 |
return undef |
|
112 |
unless defined(my $data = $loader->data($self->{index}{$rel}, $rel)); |
|
113 |
return Mojo::Asset::Memory->new->add_chunk($data); |
|
114 |
} |
|
115 | ||
116 |
sub _get_file { |
|
117 |
my ($self, $path) = @_; |
|
118 |
no warnings 'newline'; |
|
119 |
return -f $path && -r $path ? Mojo::Asset::File->new(path => $path) : undef; |
|
120 |
} |
|
121 | ||
122 |
1; |
|
123 | ||
124 |
=encoding utf8 |
|
125 | ||
126 |
=head1 NAME |
|
127 | ||
128 |
Mojolicious::Static - Serve static files |
|
129 | ||
130 |
=head1 SYNOPSIS |
|
131 | ||
132 |
use Mojolicious::Static; |
|
133 | ||
134 |
my $static = Mojolicious::Static->new; |
|
135 |
push @{$static->classes}, 'MyApp::Foo'; |
|
136 |
push @{$static->paths}, '/home/sri/public'; |
|
137 | ||
138 |
=head1 DESCRIPTION |
|
139 | ||
140 |
L<Mojolicious::Static> is a static file server with C<Range> and |
|
141 |
C<If-Modified-Since> support. |
|
142 | ||
143 |
=head1 ATTRIBUTES |
|
144 | ||
145 |
L<Mojolicious::Static> implements the following attributes. |
|
146 | ||
147 |
=head2 classes |
|
148 | ||
149 |
my $classes = $static->classes; |
|
150 |
$static = $static->classes(['main']); |
|
151 | ||
152 |
Classes to use for finding files in C<DATA> sections, first one has the |
|
153 |
highest precedence, defaults to C<main>. |
|
154 | ||
155 |
# Add another class with static files in DATA section |
|
156 |
push @{$static->classes}, 'Mojolicious::Plugin::Fun'; |
|
157 | ||
158 |
=head2 paths |
|
159 | ||
160 |
my $paths = $static->paths; |
|
161 |
$static = $static->paths(['/home/sri/public']); |
|
162 | ||
163 |
Directories to serve static files from, first one has the highest precedence. |
|
164 | ||
165 |
# Add another "public" directory |
|
166 |
push @{$static->paths}, '/home/sri/public'; |
|
167 | ||
168 |
=head1 METHODS |
|
169 | ||
170 |
L<Mojolicious::Static> inherits all methods from L<Mojo::Base> and implements |
|
171 |
the following new ones. |
|
172 | ||
173 |
=head2 dispatch |
|
174 | ||
175 |
my $bool = $static->dispatch(Mojolicious::Controller->new); |
|
176 | ||
177 |
Serve static file for L<Mojolicious::Controller> object. |
|
178 | ||
179 |
=head2 file |
|
180 | ||
181 |
my $asset = $static->file('images/logo.png'); |
|
182 |
my $asset = $static->file('../lib/MyApp.pm'); |
|
183 | ||
184 |
Get L<Mojo::Asset::File> or L<Mojo::Asset::Memory> object for a file, relative |
|
185 |
to L</"paths"> or from L</"classes">. Note that this method does not protect |
|
186 |
from traversing to parent directories. |
|
187 | ||
188 |
my $content = $static->file('foo/bar.html')->slurp; |
|
189 | ||
190 |
=head2 serve |
|
191 | ||
192 |
my $bool = $static->serve(Mojolicious::Controller->new, 'images/logo.png'); |
|
193 |
my $bool = $static->serve(Mojolicious::Controller->new, '../lib/MyApp.pm'); |
|
194 | ||
195 |
Serve a specific file, relative to L</"paths"> or from L</"classes">. Note |
|
196 |
that this method does not protect from traversing to parent directories. |
|
197 | ||
198 |
=head2 serve_asset |
|
199 | ||
200 |
$static->serve_asset(Mojolicious::Controller->new, Mojo::Asset::File->new); |
|
201 | ||
202 |
Serve a L<Mojo::Asset::File> or L<Mojo::Asset::Memory> object with C<Range> |
|
203 |
and C<If-Modified-Since> support. |
|
204 | ||
205 |
=head1 SEE ALSO |
|
206 | ||
207 |
L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>. |
|
208 | ||
209 |
=cut |