File Coverage

blib/lib/Mojolicious/Plugin/RenderFile.pm
Criterion Covered Total %
statement 61 67 91.0
branch 18 22 81.8
condition 26 37 70.2
subroutine 8 8 100.0
pod 1 1 100.0
total 114 135 84.4


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::RenderFile;
2 2     2   2084 use Mojo::Base 'Mojolicious::Plugin';
  2         4  
  2         13  
3              
4 2     2   360 use strict;
  2         11  
  2         32  
5 2     2   10 use warnings;
  2         2  
  2         55  
6 2     2   11 use File::Basename;
  2         4  
  2         158  
7 2     2   11 use Encode qw( encode decode_utf8 );
  2         3  
  2         91  
8 2     2   9 use Mojo::Util 'quote';
  2         4  
  2         1284  
9              
10             our $VERSION = '0.12';
11              
12             sub register {
13 2     2 1 72 my ( $self, $app ) = @_;
14              
15             $app->helper( 'render_file' => sub {
16 14     14   118889 my $c = shift;
17 14         56 my %args = @_;
18              
19 14 100 100     107 utf8::decode($args{filename}) if $args{filename} && !utf8::is_utf8($args{filename});
20 14 100 100     100 utf8::decode($args{filepath}) if $args{filepath} && !utf8::is_utf8($args{filepath});
21              
22 14         29 my $filename = $args{filename};
23 14   100     45 my $status = $args{status} || 200;
24 14   100     48 my $content_disposition = $args{content_disposition} || 'attachment';
25 14   50     60 my $cleanup = $args{cleanup} // 0;
26              
27             # Content type based on format
28 14 50 66     39 if ($args{format} && $args{content_type}) {
29 0         0 $c->app->log->error('You cannot provide both "format" and "content_type" option');
30 0         0 return;
31             }
32              
33 14         27 my $content_type = $args{content_type};
34 14 100 33     40 $content_type ||= $c->app->types->type( $args{format} ) if $args{format};
35 14   100     137 $content_type ||= 'application/x-download';
36              
37             # Create asset
38 14         22 my $asset;
39 14 100       36 if ( my $filepath = $args{filepath} ) {
    50          
40 9 50 33     174 unless ( -f $filepath && -r $filepath ) {
41 0         0 $c->app->log->error("Cannot read file [$filepath]. error [$!]");
42 0         0 return;
43             }
44              
45 9   66     193 $filename ||= fileparse($filepath);
46 9         74 $asset = Mojo::Asset::File->new( path => $filepath );
47 9         82 $asset->cleanup($cleanup);
48             } elsif ( $args{data} ) {
49 5   50     17 $filename ||= $c->req->url->path->parts->[-1] || 'download';
      66        
50 5         73 $asset = Mojo::Asset::Memory->new();
51 5         38 $asset->add_chunk( $args{data} );
52             } else {
53 0         0 $c->app->log->error('You must provide "data" or "filepath" option');
54 0         0 return;
55             }
56              
57             # Set response headers
58 14         131 my $headers = $c->res->content->headers();
59              
60 14         206 $filename = quote($filename); # quote the filename, per RFC 5987
61 14         163 $filename = encode("UTF-8", $filename);
62              
63 14         792 $headers->add( 'Content-Type', $content_type . ';name=' . $filename );
64 14         177 $headers->add( 'Content-Disposition', $content_disposition . ';filename=' . $filename );
65              
66             # Range
67             # Partially based on Mojolicious::Static
68 14 100       149 if ( my $range = $c->req->headers->range ) {
69 6         128 my $start = 0;
70 6         20 my $size = $asset->size;
71 6 50       405 my $end = $size - 1 >= 0 ? $size - 1 : 0;
72              
73             # Check range
74 6 100 66     52 if ( $range =~ m/^bytes=(\d+)-(\d+)?/ && $1 <= $end ) {
75 5         13 $start = $1;
76 5 100 66     26 $end = $2 if defined $2 && $2 <= $end;
77              
78 5         11 $status = 206;
79 5         20 $headers->add( 'Content-Length' => $end - $start + 1 );
80 5         62 $headers->add( 'Content-Range' => "bytes $start-$end/$size" );
81             } else {
82             # Not satisfiable
83 1         7 return $c->rendered(416);
84             }
85              
86             # Set range for asset
87 5         56 $asset->start_range($start)->end_range($end);
88             } else {
89 8         214 $headers->add( 'Content-Length' => $asset->size );
90             }
91              
92             # Stream content directly from file
93 13         999 $c->res->content->asset($asset);
94 13         227 return $c->rendered($status);
95 2         18 } );
96             }
97              
98             1;
99              
100             =head1 NAME
101              
102             Mojolicious::Plugin::RenderFile - "render_file" helper for Mojolicious
103              
104             =head1 SYNOPSIS
105              
106             # Mojolicious
107             $self->plugin('RenderFile');
108              
109             # Mojolicious::Lite
110             plugin 'RenderFile';
111              
112             # In controller
113             $self->render_file('filepath' => '/tmp/files/file.pdf'); # file name will be "file.pdf"
114              
115             # Provide any file name
116             $self->render_file('filepath' => '/tmp/files/file.pdf', 'filename' => 'report.pdf');
117              
118             # Render data from memory as file
119             $self->render_file('data' => 'some data here', 'filename' => 'report.pdf');
120              
121             # Open file in browser(do not show save dialog)
122             $self->render_file(
123             'filepath' => '/tmp/files/file.pdf',
124             'format' => 'pdf', # will change Content-Type "application/x-download" to "application/pdf"
125             'content_disposition' => 'inline', # will change Content-Disposition from "attachment" to "inline"
126             'cleanup' => 1, # delete file after completed
127             );
128              
129             =head1 DESCRIPTION
130              
131             L is a L plugin that adds "render_file" helper. It does not read file in memory and just streaming it to a client.
132              
133             =head1 HELPERS
134              
135             =head2 C
136              
137             $self->render_file(filepath => '/tmp/files/file.pdf', 'filename' => 'report.pdf' );
138              
139             With this helper you can easily provide files for download. By default "Content-Type" header is "application/x-download" and "content_disposition" option value is "attachment".
140             Therefore, a browser will ask where to save file. You can provide "format" option to change "Content-Type" header.
141              
142              
143             =head3 Supported Options:
144              
145             =over
146              
147             =item C
148              
149             Path on the filesystem to the file. You must always pass "filepath" or "data" option
150              
151             =item C
152              
153             Binary content which will be transferred to browser. You must always pass "filepath" or "data" option
154              
155             =item C (optional)
156              
157             Browser will use this name for saving the file
158              
159             =item C (optional)
160              
161             The "Content-Type" header is based on the MIME type mapping of the "format" option value. These mappings can be easily extended or changed with L.
162              
163             By default "Content-Type" header is "application/x-download"
164              
165             =item C (optional)
166              
167             Tells browser how to present the file.
168              
169             "attachment" (default) - is for dowloading
170              
171             "inline" - is for showing file inline
172              
173             =item C (optional)
174              
175             Indicates if the file should be deleted when rendering is complete
176              
177             =back
178              
179             This plugin respects HTTP Range headers.
180              
181             =head1 AUTHOR
182              
183             Viktor Turskyi
184              
185             =head1 CONTRIBUTORS
186              
187             Nils Diewald (Akron)
188             Danil Greben (SDSWanderer)
189              
190             =head1 BUGS
191              
192             Please report any bugs or feature requests to Github L
193              
194             =head1 SEE ALSO
195              
196             L, L, L.
197              
198             Copyright 2011 Viktor Turskyi
199              
200             This program is free software; you can redistribute it and/or modify it
201             under the terms of either: the GNU General Public License as published
202             by the Free Software Foundation; or the Artistic License.
203              
204             See http://dev.perl.org/licenses/ for more information.
205              
206             =cut