File Coverage

blib/lib/Catalyst/Plugin/UploadProgress.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::UploadProgress;
2 1     1   1382 use Moose::Role;
  0            
  0            
3             use Catalyst::Exception;
4             use MooseX::RelatedClassRoles ();
5             use namespace::autoclean;
6              
7             our $VERSION = '0.06';
8              
9             requires qw/
10             prepare_body_chunk
11             prepare_body
12             dispatch
13             setup_finalize
14             /;
15              
16              
17             around _build_request_constructor_args => sub {
18             my ($orig, $self, @args) = @_;
19             my $p = $self->$orig(@args);
20             $p->{_cache} = $self->cache;
21             return $p;
22             };
23              
24             around 'prepare_body' => sub {
25             my $orig = shift;
26             my $c = shift;
27              
28             # Detect if the user stopped the upload, prepare_body will die with an invalid
29             # content-length error
30              
31             my $croaked;
32              
33             {
34             no warnings 'redefine';
35             local *Carp::croak = sub {
36             $croaked = shift;
37             };
38              
39             $c->$orig(@_);
40             }
41              
42             if ( $croaked ) {
43             if ( my $id = $c->req->query_parameters->{progress_id} ) {
44             $c->log->info( "UploadProgress: User aborted upload $id" );
45              
46             # Update progress to flag this so javascript will stop polling
47             my $progress = $c->cache->get( 'upload_progress_' . $id ) || {};
48              
49             $progress->{aborted} = 1;
50              
51             $c->cache->set( 'upload_progress_' . $id, $progress );
52             }
53              
54             # rethrow the error
55             Catalyst::Exception->throw( $croaked );
56             }
57             };
58              
59             around 'dispatch' => sub {
60             my $orig = shift;
61             my $c = shift;
62              
63             # if the URI query string is ?progress_id=<id> intercept the request
64             # and display the progress JSON.
65             my $query = $c->req->uri->path_query;
66             if ( $c->req->method eq 'GET' && $query =~ m{\?progress_id=([a-f0-9]{32})$} ) {
67             return $c->upload_progress_output( $1 );
68             }
69              
70             return $c->$orig(@_);
71             };
72              
73             after 'setup_finalize' => sub {
74             my $c = shift;
75              
76             unless ( $c->can('cache') ) {
77             Catalyst::Exception->throw(
78             message => 'UploadProgress requires a cache plugin.'
79             );
80             }
81             Class::MOP::class_of('MooseX::RelatedClassRoles')
82             ->apply($c->meta, name => 'request', require_class_accessor => 0);
83             $c->apply_request_class_roles('Catalyst::Plugin::UploadProgress::Role::Request');
84             };
85              
86             sub upload_progress {
87             my ( $c, $upload_id ) = @_;
88              
89             return $c->cache->get( 'upload_progress_' . $upload_id );
90             }
91              
92             sub upload_progress_output {
93             my ( $c, $upload_id ) = @_;
94              
95             $upload_id ||= $c->req->params->{progress_id};
96              
97             my $progress = $c->upload_progress( $upload_id );
98              
99             # there could be a race condition where /progress is called before
100             # the upload has passed through prepare_body_chunk. Set a default
101             # progress hash in this case. Once through prepare_body_chunk,
102             # the values will be correct.
103             if ( !ref $progress ) {
104             $progress = {
105             received => 0,
106             size => -1,
107             };
108             }
109              
110             # format the progress data as JSON
111             my $json = '{"size":%d,"received":%d,"aborted":%d}';
112             my $output = sprintf $json,
113             $progress->{size},
114             $progress->{received},
115             $progress->{aborted} || 0;
116              
117             $c->response->headers->header( Pragma => 'no-cache' );
118             $c->response->headers->header( Expires
119             => 'Thu, 01 Jan 1970 00:00:00 GMT' );
120             $c->response->headers->header( 'Cache-Control'
121             => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0' );
122              
123             $c->response->content_type( 'text/x-json' );
124             $c->response->body( $output );
125             }
126              
127             sub upload_progress_javascript {
128             my $c = shift;
129              
130             require Catalyst::Plugin::UploadProgress::Static;
131              
132             my @output;
133             push @output,
134             '<style type="text/css">' . "\n"
135             . Catalyst::Plugin::UploadProgress::Static->upload_progress_css
136             . '</style>';
137              
138             push @output,
139             '<script type="text/javascript">' . "\n"
140             . Catalyst::Plugin::UploadProgress::Static->upload_progress_js
141             . '</script>';
142              
143             push @output,
144             '<script type="text/javascript">' . "\n"
145             . Catalyst::Plugin::UploadProgress::Static->upload_progress_jmpl_js
146             . '</script>';
147              
148             return join "\n", @output;
149             }
150              
151             1;
152              
153             =head1 NAME
154              
155             Catalyst::Plugin::UploadProgress - Realtime file upload information
156              
157             =head1 SYNOPSIS
158              
159             use Catalyst;
160             MyApp->setup( qw/Static::Simple Cache::FastMmap UploadProgress/ );
161              
162             # On the HTML page with the upload form, include the progress
163             # JavaScript and CSS. These are available via a single method
164             # if you are lazy.
165             <html>
166             <head>
167             [% c.upload_progress_javascript %]
168             </head>
169             ...
170              
171             # For better performance, copy these 3 files from the UploadProgress
172             # distribution to your static directory and include them normally.
173             <html>
174             <head>
175             <link href="/static/css/progress.css" rel="stylesheet" type="text/css" />
176             <script src="/static/js/progress.js" type="text/javascript"></script>
177             <script src="/static/js/progress.jmpl.js" type="text/javascript"></script>
178             </head>
179             ...
180              
181             # Create the upload form with an onsubmit action that creates
182             # the Ajax progress bar. Note the empty div following the form
183             # where the progress bar will be inserted.
184             <form action='/upload'
185             method="post"
186             enctype="multipart/form-data"
187             onsubmit="return startEmbeddedProgressBar(this)">
188            
189             <input type="file" name="file" />
190             <input type="submit" />
191             </form>
192             <div id='progress'></div>
193              
194             # No special code is required within your application, just handle
195             # the upload as usual.
196             sub upload : Local {
197             my ( $self, $c ) = @_;
198              
199             my $upload = $c->request->uploads->{file};
200             $upload->copy_to( '/some/path/' . $upload->filename );
201             }
202              
203             =head1 DESCRIPTION
204              
205             This plugin is a simple, transparent method for displaying a
206             progress bar during file uploads.
207              
208             =head1 DEMO
209              
210             Please see the example/Upload directory in the distribution for a working
211             example Catalyst application with upload progress. Since Upload Progress
212             requires 2 concurrent connections (one for the upload and one for the
213             Ajax, you will need to use either script/upload_poe.pl (which requires
214             L<Catalyst::Engine::HTTP::POE> >= 0.02) or script/upload_server.pl -f.
215             The -f enables forking for each new request.
216              
217             =head1 ENGINE SUPPORT
218              
219             The included demo application has been tested and is known to work on
220             the following setups with Catalyst 5.7003:
221              
222             C::E::HTTP (server.pl) with -f flag (OSX)
223             C::E::HTTP::POE 0.06 (OSX)
224             C::E::Apache2::MP20 1.07 with Apache 2.0.58, mod_perl 2.0.2 (OSX)
225             C::E::FastCGI with Apache 2.0.55, mod_fastcgi 2.4.2 (Ubuntu)
226              
227             =head1 INTERNAL METHODS
228              
229             You don't need to know about these methods, but they are documented
230             here for developers.
231              
232             =head2 upload_progress( $progress_id )
233              
234             Returns the data structure associated with the given progress ID.
235             Currently the data is a hashref with the total size of the upload
236             and the amount of bytes received so far.
237              
238             {
239             size => 110636659,
240             received => 134983
241             }
242              
243             =head2 upload_progress_output
244              
245             Returns a JSON response containing the upload progress data.
246              
247             =head2 upload_progress_javascript
248              
249             Inlines the necessary JavaScript and CSS code into your page. For better
250             performance, you should copy the files into your application as displayed
251             above in the Synopsis.
252              
253             =head1 EXTENDED METHODS
254              
255             =head2 prepare_body ( $c )
256              
257             Detects if the user aborted the upload.
258              
259             =head2 prepare_body_chunk ( $chunk )
260              
261             Takes each chunk of incoming upload data and updates the upload progress
262             record with new information.
263              
264             =head2 dispatch
265              
266             Watches for a URI ending with '?progress_id=<id>' and returns the
267             JSON output from C</upload_progress_output>.
268              
269             =head2 setup
270              
271             =head1 AUTHOR
272              
273             Andy Grundman, <andy@hybridized.org>
274              
275             NEXT to Moose::Role conversion by Toby Corkindale, <tjc@cpan.org>, blame him
276             for any faults there..
277              
278             =head1 THANKS
279              
280             The authors of L<Apache2::UploadProgress>, for the progress.js and
281             progress.css code:
282              
283             Christian Hansen <chansen@cpan.org>
284             Cees Hek <ceeshek@cpan.org>
285              
286             =head1 COPYRIGHT
287              
288             This program is free software, you can redistribute it and/or modify it under
289             the same terms as Perl itself.
290              
291             =cut
292