File Coverage

blib/lib/Apache2/UploadProgress.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Apache2::UploadProgress;
2              
3 1     1   1073 use strict;
  1         3  
  1         42  
4 1     1   6 use warnings;
  1         2  
  1         31  
5 1     1   16 use bytes;
  1         2  
  1         7  
6              
7 1     1   3260 use Apache2::Const -compile => qw( OK DECLINED NOT_FOUND M_POST RSRC_CONF TAKE1 );
  0            
  0            
8             use Apache2::Filter qw[];
9             use Apache2::Module qw[];
10             use Apache2::RequestRec qw[];
11             use Apache2::RequestIO qw[];
12             use Apache2::Response qw[];
13             use Apache2::ServerUtil qw[];
14             use APR::Const -compile => qw( SUCCESS );
15             use APR::Brigade qw[];
16             use APR::Bucket qw[];
17             use APR::Table qw[];
18             use Cache::FastMmap qw[];
19             use File::Spec qw[];
20             use HTTP::Headers::Util qw[split_header_words];
21             use Time::HiRes qw[sleep];
22              
23             our $VERSION = 0.2;
24              
25             our $CACHE = Cache::FastMmap->new(
26             share_file => $ENV{UPLOADPROGRESS_SHARE_FILE} || File::Spec->catfile( File::Spec->tmpdir, 'Apache2-UploadProgress' ),
27             init_file => 1,
28             raw_values => 1,
29             page_size => $ENV{UPLOADPROGRESS_PAGE_SIZE} || '64k',
30             num_pages => $ENV{UPLOADPROGRESS_NUM_PAGES} || '89',
31             ) or die qq/Failed to create a new instance of Cache::FastMmap. Reason: '$!'/;
32              
33             our $DIRECTIVES = [
34             {
35             name => 'UploadProgressBaseURI',
36             req_override => Apache2::Const::RSRC_CONF,
37             args_how => Apache2::Const::TAKE1,
38             errmsg => 'Absolute or relative URI to extras without trailing forward slash',
39             }
40             ];
41              
42             our ( $TEMPLATES, $MIMES, $HAS_BASEURI );
43              
44             if ( $ENV{MOD_PERL} ) {
45              
46             Apache2::Module::add( __PACKAGE__, $DIRECTIVES );
47              
48             if ( Apache2::ServerUtil::restart_count() > 1
49             && Apache2::Module::loaded('mod_alias.c')
50             && Apache2::Module::loaded('mod_mime.c') ) {
51              
52             my $config = [
53             sprintf( 'Alias /UploadProgress %s/extra', substr( __FILE__, 0, -3 ) ),
54             '',
55             'SetHandler default-handler',
56             Apache2::Module::loaded('mod_expires.c')
57             ? ( 'ExpiresActive On', 'ExpiresDefault "access plus 1 day"')
58             : (),
59             '',
60             '',
61             'SetHandler modperl',
62             'PerlResponseHandler Apache2::UploadProgress->progress',
63             ''
64             ];
65              
66             Apache2::ServerUtil->server->add_config($config);
67              
68             $HAS_BASEURI = 1;
69             }
70             }
71              
72             $TEMPLATES->{html} = <<'EOF';
73            
74             PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
75             "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
76            
77            
78             UploadProgress
79            
80            
81            
82            
83            
84            

Upload Progress

85            
86            
87            
88             EOF
89              
90             $TEMPLATES->{json} = <<'EOF';
91             {"size":%d,"received":%d}
92             EOF
93              
94             $TEMPLATES->{text} = <<'EOF';
95             size: %d
96             received: %d
97             EOF
98              
99             $TEMPLATES->{yaml} = <<'EOF';
100             ---
101             size: %d
102             received: %d
103             EOF
104              
105             $TEMPLATES->{xml} = <<'EOF';
106            
107             %s
108             %d
109             %d
110            
111             EOF
112              
113             $MIMES = {
114             'application/x-json' => sub { sprintf( $TEMPLATES->{json}, @_ ) },
115             'application/x-yaml' => sub { sprintf( $TEMPLATES->{yaml}, @_ ) },
116             'application/xhtml+xml' => sub { sprintf( $TEMPLATES->{html}, @_ ) },
117             'application/xml' => \&xml_template,
118             'text/html' => sub { sprintf( $TEMPLATES->{html}, @_ ) },
119             'text/plain' => sub { sprintf( $TEMPLATES->{text}, @_ ) },
120             'text/x-json' => sub { sprintf( $TEMPLATES->{json}, @_ ) },
121             'text/x-yaml' => sub { sprintf( $TEMPLATES->{yaml}, @_ ) },
122             'text/xml' => \&xml_template,
123             };
124              
125             sub xml_template {
126             my ($size, $received, $r) = @_;
127             my $xsl = '';
128             my $xsd = '';
129             if ( my $uri = Apache2::UploadProgress->base_uri($r) ) {
130             $xsl = "\n";
131             $xsd = ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="${uri}/progress.xsd"';
132             }
133             return sprintf( $TEMPLATES->{xml}, $xsl, $xsd, $size, $received);
134             }
135              
136              
137             sub register_mime : method {
138             my ( $class, $mime, $callback ) = @_;
139             $MIMES->{ lc $mime } = $callback;
140             }
141              
142             sub UploadProgressBaseURI {
143             my ( $self, $parms, $uri ) = @_;
144             $self->{UploadProgressBaseURI} = $uri;
145             }
146              
147             sub config {
148             my ( $class, $r ) = @_;
149             return Apache2::Module::get_config( __PACKAGE__, $r->server, $r->per_dir_config );
150             }
151              
152             sub base_uri {
153             my ( $class, $r ) = @_;
154              
155             if ( $r ) {
156             my $config = $class->config($r);
157             return $config->{UploadProgressBaseURI} if $config->{UploadProgressBaseURI};
158             }
159              
160             if ( $HAS_BASEURI ) {
161             return '/UploadProgress';
162             }
163              
164             return undef;
165             }
166              
167             sub progress_id {
168             my ( $class, $r ) = @_;
169              
170             return $r->headers_in->get('X-Upload-ID')
171             || $r->headers_in->get('X-Progress-ID') # lighttpd compat
172             || ( $r->unparsed_uri =~ m/\?([a-fA-F0-9]{32})$/ )[0] # lighttpd compat
173             || ( $r->unparsed_uri =~ m/(?:progress|upload)_id=([a-fA-F0-9]{32})/ )[0];
174             }
175              
176             sub fetch_progress {
177             my ( $class, $progress_id ) = @_;
178              
179             my $progress = $CACHE->get($progress_id)
180             or return undef;
181              
182             return [ unpack( 'LL', $progress ) ];
183             }
184              
185             sub store_progress {
186             my ( $class, $progress_id, $progress ) = @_;
187              
188             return $CACHE->set( $progress_id => pack( 'LL', @$progress ) );
189             }
190              
191             sub track_progress {
192             my ( $class, $f, $bb, $mode, $block, $readbytes ) = @_;
193              
194             unless ( $f->ctx ) {
195              
196             my $ctx = [];
197              
198             $ctx->[0] = $class->progress_id( $f->r )
199             or return Apache2::Const::DECLINED;
200              
201             $ctx->[1]->[0] = $f->r->headers_in->get('Content-Length') || 0;
202             $ctx->[1]->[1] = 0;
203              
204             $f->ctx($ctx);
205              
206             $class->store_progress( @{ $f->ctx } );
207             }
208              
209             my $rv = $f->next->get_brigade( $bb, $mode, $block, $readbytes );
210              
211             unless ( $rv == APR::Const::SUCCESS ) {
212             return $rv;
213             }
214              
215             $f->ctx->[1]->[1] += $bb->length;
216              
217             $class->store_progress( @{ $f->ctx } );
218              
219             return Apache2::Const::OK;
220             }
221              
222             sub handler : method {
223             my ( $class, $r ) = @_;
224              
225             $r->method_number == Apache2::Const::M_POST
226             or return Apache2::Const::DECLINED;
227              
228             $class->progress_id($r)
229             or return Apache2::Const::DECLINED;
230              
231             $r->add_input_filter( $class . '->track_progress' );
232              
233             return Apache2::Const::OK;
234             }
235              
236             sub progress : method {
237             my ( $class, $r ) = @_;
238              
239             my $progress_id = $class->progress_id($r)
240             or return Apache2::Const::NOT_FOUND;
241            
242             my $progress = undef;
243             my $tries = 16; # wait a max of 4 seconds for the upload to start
244            
245             while ( $tries && !$progress ) {
246              
247             $progress = $class->fetch_progress($progress_id)
248             or sleep(0.250);
249            
250             $tries--;
251             }
252            
253             unless ( $progress ) {
254             return Apache2::Const::NOT_FOUND;
255             }
256              
257             my $content_type = 'text/xml';
258              
259             if ( my $accept_header = $r->headers_in->get('Accept') ) {
260              
261             my %accept = ();
262             my $counter = 0;
263              
264             foreach my $pair ( split_header_words($accept_header) ) {
265              
266             my ( $type, $qvalue ) = @{ $pair }[0,3];
267              
268             unless ( defined $qvalue ) {
269             $qvalue = 1 - ( ++$counter / 1000 );
270             }
271              
272             $accept{ $type } = sprintf( '%.3f', $qvalue );
273             }
274              
275             foreach my $type ( sort { $accept{$b} <=> $accept{$a} } keys %accept ) {
276              
277             if ( exists $MIMES->{$type} ) {
278             $content_type = $type;
279             last;
280             }
281             }
282             }
283              
284             $r->headers_out->set( 'Vary' => 'Accept' );
285             $r->headers_out->set( 'Pragma' => 'no-cache' );
286             $r->headers_out->set( 'Expires' => 'Thu, 01 Jan 1970 00:00:00 GMT' );
287             $r->headers_out->set( 'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0' );
288              
289             my $callback = $MIMES->{$content_type};
290             my $content = $callback->( @$progress, $r );
291              
292             $r->content_type($content_type);
293             $r->set_content_length( length $content );
294             $r->write($content);
295              
296             return Apache2::Const::OK;
297             }
298              
299             1;
300              
301             __END__