Skip to content

Commit

Permalink
added reply->asset helper
Browse files Browse the repository at this point in the history
  • Loading branch information
kraih committed Sep 13, 2014
1 parent d8707ba commit 22d60dd
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 26 deletions.
5 changes: 4 additions & 1 deletion Changes
Expand Up @@ -2,7 +2,10 @@
5.41 2014-09-13
- Deprecated Mojolicious::Controller::render_static in favor of
reply->static helper.
- Added reply->static helper to Mojolicious::Plugin::DefaultHelpers.
- Added mtime attribute to Mojo::Asset::Memory.
- Added mtime method to Mojo::Asset and Mojo::Asset::File.
- Added reply->asset and reply->static helpers to
Mojolicious::Plugin::DefaultHelpers.
- Fixed bug in Mojo::UserAgent where connections would sometimes not get
closed correctly.

Expand Down
8 changes: 8 additions & 0 deletions lib/Mojo/Asset.pm
Expand Up @@ -15,6 +15,7 @@ sub is_file {undef}
sub is_range { !!($_[0]->end_range || $_[0]->start_range) }

sub move_to { croak 'Method "move_to" not implemented by subclass' }
sub mtime { croak 'Method "mtime" not implemented by subclass' }
sub size { croak 'Method "size" not implemented by subclass' }
sub slurp { croak 'Method "slurp" not implemented by subclass' }

Expand All @@ -35,6 +36,7 @@ Mojo::Asset - HTTP content storage base class
sub contains {...}
sub get_chunk {...}
sub move_to {...}
sub mtime {...}
sub size {...}
sub slurp {...}
Expand Down Expand Up @@ -108,6 +110,12 @@ Check if asset has a L</"start_range"> or L</"end_range">.
Move asset data into a specific file. Meant to be overloaded in a subclass.
=head2 mtime
my $mtime = $asset->mtime;
Modification time of asset. Meant to be overloaded in a subclass.
=head2 size
my $size = $asset->size;
Expand Down
14 changes: 10 additions & 4 deletions lib/Mojo/Asset/File.pm
Expand Up @@ -120,16 +120,16 @@ sub move_to {
return $self->path($to)->cleanup(0);
}

sub size {
return 0 unless defined(my $file = shift->path);
return -s $file;
}
sub mtime { _stat(shift->path, 1) }
sub size { _stat(shift->path, 0) }

sub slurp {
return '' unless defined(my $file = shift->path);
return Mojo::Util::slurp $file;
}

sub _stat { $_[0] ? $_[1] ? (stat $_[0])[9] : -s $_[0] : 0 }

1;

=encoding utf8
Expand Down Expand Up @@ -233,6 +233,12 @@ True.
Move asset data into a specific file and disable L</"cleanup">.
=head2 mtime
my $mtime = $file->mtime;
Modification time of asset.
=head2 size
my $size = $file->size;
Expand Down
11 changes: 11 additions & 0 deletions lib/Mojo/Asset/Memory.pm
Expand Up @@ -4,8 +4,12 @@ use Mojo::Base 'Mojo::Asset';
use Mojo::Asset::File;
use Mojo::Util 'spurt';

# Last modified default
my $MTIME = time;

has 'auto_upgrade';
has max_memory_size => sub { $ENV{MOJO_MAX_MEMORY_SIZE} || 262144 };
has mtime => sub {$MTIME};

sub add_chunk {
my ($self, $chunk) = @_;
Expand Down Expand Up @@ -114,6 +118,13 @@ Maximum size in bytes of data to keep in memory before automatically upgrading
to a L<Mojo::Asset::File> object, defaults to the value of the
C<MOJO_MAX_MEMORY_SIZE> environment variable or C<262144> (256KB).
=head2 mtime
my $mtime = $mem->mtime;
$mem = $mem->mtime(1408567500);
Modification time of asset, defaults to the time this class was loaded.
=head1 METHODS
L<Mojo::Asset::Memory> inherits all methods from L<Mojo::Asset> and implements
Expand Down
16 changes: 13 additions & 3 deletions lib/Mojolicious/Guides/Rendering.pod
Expand Up @@ -975,9 +975,19 @@ L<Mojolicious::Plugin::DefaultHelpers/"reply-E<gt>static">.

=head2 Custom responses

For entirely custom responses to, for example, stream content directly from
files, you can use L<Mojolicious::Controller/"rendered"> to tell the renderer
that a response has been generated.
Most generated content gets served through L<Mojo::Asset::File> and
L<Mojo::Asset::Memory> objects in responses. For somewhat static content, like
cached JSON data, you can create your own and use the helper
L<Mojolicious::Plugin::DefaultHelpers/"reply-E<gt>asset"> to serve it while
allowing content negotiation to be performed with C<Range>,
C<If-Modified-Since> and C<If-None-Match> headers.

$c->res->headers->content_type('text/plain');
$c->reply->asset(Mojo::Asset::File->new(path => '/etc/passwd'));

For even more control you can also just skip the helper and use
L<Mojolicious::Controller/"rendered"> to tell the renderer when you're done
generating a response.

$c->res->headers->content_type('text/plain');
$c->res->content->asset(Mojo::Asset::File->new(path => '/etc/passwd'));
Expand Down
37 changes: 25 additions & 12 deletions lib/Mojolicious/Plugin/DefaultHelpers.pm
Expand Up @@ -20,23 +20,29 @@ sub register {
$app->helper($name => sub { shift->stash($name, @_) });
}

$app->helper(accepts => sub { $_[0]->app->renderer->accepts(@_) });
$app->helper(b => sub { shift; Mojo::ByteStream->new(@_) });
$app->helper(c => sub { shift; Mojo::Collection->new(@_) });
$app->helper(config => sub { shift->app->config(@_) });

$app->helper($_ => $self->can("_$_"))
for qw(accepts content content_for csrf_token current_route delay),
for qw(content content_for csrf_token current_route delay),
qw(inactivity_timeout is_fresh url_with);
$app->helper(b => sub { shift; Mojo::ByteStream->new(@_) });
$app->helper(c => sub { shift; Mojo::Collection->new(@_) });
$app->helper(config => sub { shift->app->config(@_) });
$app->helper(dumper => sub { shift; dumper(@_) });
$app->helper(include => sub { shift->render_to_string(@_) });

$app->helper(dumper => sub { shift; dumper(@_) });
$app->helper(include => sub { shift->render_to_string(@_) });

$app->helper("reply.$_" => $self->can("_$_")) for qw(asset static);

$app->helper('reply.exception' => sub { _development('exception', @_) });
$app->helper('reply.not_found' => sub { _development('not_found', @_) });
$app->helper('reply.static' => \&_static);
$app->helper(ua => sub { shift->app->ua });
$app->helper(ua => sub { shift->app->ua });
}

sub _accepts {
sub _asset {
my $c = shift;
return $c->app->renderer->accepts($c, @_);
$c->app->static->serve_asset($c, @_);
$c->rendered;
}

sub _content {
Expand Down Expand Up @@ -67,8 +73,7 @@ sub _csrf_token {

sub _current_route {
return '' unless my $endpoint = shift->match->endpoint;
return $endpoint->name unless @_;
return $endpoint->name eq shift;
return @_ ? $endpoint->name eq shift : $endpoint->name;
}

sub _delay {
Expand Down Expand Up @@ -365,6 +370,14 @@ L</"stash">.
Alias for L<Mojolicious::Controller/"param">.
=head2 reply->asset
$c->reply->asset(Mojo::Asset::File->new);
Reply with L<Mojo::Asset::File> or L<Mojo::Asset::Memory> object using
L<Mojolicious::Static/"serve_asset">, and perform content negotiation with
C<Range>, C<If-Modified-Since> and C<If-None-Match> headers.
=head2 reply->exception
$c = $c->reply->exception('Oops!');
Expand Down
5 changes: 1 addition & 4 deletions lib/Mojolicious/Static.pm
Expand Up @@ -12,9 +12,6 @@ use Mojo::Util 'md5_sum';
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');
Expand Down Expand Up @@ -98,7 +95,7 @@ sub serve_asset {
# Last-Modified and ETag
my $res = $c->res;
$res->code(200)->headers->accept_ranges('bytes');
my $mtime = $asset->is_file ? (stat $asset->path)[9] : $MTIME;
my $mtime = $asset->mtime;
my $options = {etag => md5_sum($mtime), last_modified => $mtime};
return $res->code(304) if $self->is_fresh($c, $options);

Expand Down
7 changes: 7 additions & 0 deletions t/mojo/asset.t
Expand Up @@ -10,12 +10,14 @@ use Mojo::Asset::Memory;
# File asset
my $file = Mojo::Asset::File->new;
is $file->size, 0, 'file is empty';
is $file->mtime, 0, 'file is empty';
is $file->slurp, '', 'file is empty';
$file->add_chunk('abc');
is $file->contains('abc'), 0, '"abc" at position 0';
is $file->contains('bc'), 1, '"bc" at position 1';
is $file->contains('db'), -1, 'does not contain "db"';
is $file->size, 3, 'right size';
is $file->mtime, (stat $file->path)[9], 'right mtime';

# Cleanup
my $path = $file->path;
Expand All @@ -30,6 +32,10 @@ is $mem->contains('abc'), 0, '"abc" at position 0';
is $mem->contains('bc'), 1, '"bc" at position 1';
is $mem->contains('db'), -1, 'does not contain "db"';
is $mem->size, 3, 'right size';
ok $mem->mtime > (time - 100), 'right mtime';
is $mem->mtime, Mojo::Asset::Memory->new->mtime, 'same mtime';
my $mtime = $mem->mtime;
is $mem->mtime($mtime + 23)->mtime, $mtime + 23, 'right mtime';

# Empty file asset
$file = Mojo::Asset::File->new;
Expand Down Expand Up @@ -230,6 +236,7 @@ $file = Mojo::Asset::File->new(cleanup => 0)->add_chunk('test');
ok $file->is_file, 'stored in file';
is $file->slurp, 'test', 'right content';
is $file->size, 4, 'right size';
is $file->mtime, (stat $file->path)[9], 'right mtime';
is $file->contains('es'), 1, '"es" at position 1';
$path = $file->path;
undef $file;
Expand Down
24 changes: 22 additions & 2 deletions t/mojolicious/static_lite_app.t
Expand Up @@ -6,6 +6,7 @@ BEGIN {
}

use Test::More;
use Mojo::Asset::Memory;
use Mojo::Date;
use Mojolicious::Lite;
use Test::Mojo;
Expand All @@ -27,6 +28,12 @@ get '/etag' => sub {
: $c->render(text => 'I ♥ Mojolicious!');
};

get '/asset' => sub {
my $c = shift;
my $mem = Mojo::Asset::Memory->new->add_chunk('I <3 Assets!');
$c->reply->asset($mem);
};

my $t = Test::Mojo->new;

# Freshness
Expand Down Expand Up @@ -150,9 +157,22 @@ $t->get_ok('/etag')->status_is(200)->header_is(Server => 'Mojolicious (Perl)')
->header_is(ETag => '"abc"')->content_is('I ♥ Mojolicious!');

# Stale content
$t->get_ok('/etag' => {'If-None-Match' => '"abc"'})
$t->get_ok('/etag' => {'If-None-Match' => '"abc"'})->status_is(304)
->header_is(Server => 'Mojolicious (Perl)')->header_is(ETag => '"abc"')
->status_is(304)->content_is('');
->content_is('');

# Fresh asset
$t->get_ok('/asset')->status_is(200)
->header_is(Server => 'Mojolicious (Perl)')->content_is('I <3 Assets!');
my $etag = $t->tx->res->headers->etag;

# Stale asset
$t->get_ok('/asset' => {'If-None-Match' => $etag})->status_is(304)
->header_is(Server => 'Mojolicious (Perl)')->content_is('');

# Partial asset
$t->get_ok('/asset' => {'Range' => 'bytes=3-5'})->status_is(206)
->header_is(Server => 'Mojolicious (Perl)')->content_is('3 A');

# Empty file
$t->get_ok('/hello4.txt')->status_is(200)
Expand Down

0 comments on commit 22d60dd

Please sign in to comment.