File Coverage

blib/lib/HTTP/RangeSaver.pm
Criterion Covered Total %
statement 199 211 94.3
branch 49 70 70.0
condition 18 29 62.0
subroutine 19 22 86.3
pod 9 16 56.2
total 294 348 84.4


line stmt bran cond sub pod time code
1             package HTTP::RangeSaver;
2              
3 1     1   96779 use strict;
  1         2  
  1         116  
4              
5             our $VERSION='0.01';
6              
7             =head1 NAME
8              
9             HTTP::RangeSaver - handle partial content HTTP responses
10              
11             =head1 SYNOPSIS
12              
13             use LWP;
14             use HTTP::RangeSaver;
15              
16             open(my $fh,'+<','example.mpeg') || die $!;
17             my $req=new HTTP::Request
18             (GET => 'http://www.example.com/example.mpeg');
19             $req->header(Range => 'bytes='.(-s $fh).'-');
20             my $saver=new HTTP::RangeSaver($fh);
21             my $ua=new LWP::UserAgent;
22             my $resp=$ua->request($req,$saver->get_callback());
23              
24             =head1 DESCRIPTION
25              
26             HTTP::RangeSaver is a helper class for updating an existing file with
27             data from a partial content HTTP response. It understands both of the
28             partial content formats defined in RFC 2616 (a single Content-Range
29             header or a multipart/byteranges Content-Type). For convenience, it
30             also handles complete content HTTP responses (status 200 or 203 rather
31             than 206).
32              
33             =cut
34              
35             use fields
36 1         9 qw(fh delta truncate
37             require_partial require_length require_resource
38             methods expected buffer start_boundary end_boundary
39 1     1   1918 type length written partheaders ranges);
  1         2360  
40              
41             use Symbol
42 1     1   3834 qw(qualify_to_ref);
  1         2476  
  1         117  
43              
44 1     1   13 use HTTP::Headers;
  1         3  
  1         35  
45             use HTTP::Headers::Util
46 1     1   2006 qw(split_header_words);
  1         1302  
  1         3659  
47              
48             =head1 CONSTRUCTOR
49              
50             =over
51              
52             =item my $saver=HTTP::RangeSaver->new($fh,%options);
53              
54             $fh is an open filehandle. It must allow seeking and writing.
55              
56             %options is a list of key/value pairs for modifying the saver's
57             behaviour.
58              
59             =over
60              
61             =item truncate
62              
63             Pass a true value to make the saver truncate the file to match the full
64             length of the returned entity. Ignored if the server doesn't report a
65             definite length.
66              
67             =item require_length
68              
69             Pass a true value to make the saver die if the server doesn't report a
70             definite full length for the returned entity.
71              
72             =item require_partial
73              
74             Pass a true value to make the saver die if the server returns a complete
75             entity rather than a partial one.
76              
77             =item require_resource
78              
79             Pass a true value to make the saver die if the server returns an entity
80             that doesn't represent the requested resource (i.e. a 2xx status code
81             other than 200, 203, or 206). This should never happen for a GET
82             request.
83              
84             =item delta
85              
86             A adjustment to be applied to all file offsets in the destination file.
87              
88             =back
89              
90             =back
91              
92             =cut
93              
94             sub new
95             {
96 13     13 1 5822 my($class,$fh,%params)=@_;
97 13         17 my __PACKAGE__ $self;
98              
99 13         38 $self=fields::new($class);
100 13         5232 $self->{fh}=qualify_to_ref($fh,caller());
101 13 50       127 if (exists($params{delta})) {
102 0         0 $self->{delta}=int($params{delta});
103             } else {
104 13         25 $self->{delta}=0;
105             }
106 13   50     40 $self->{truncate}=$params{truncate} && 1;
107 13   100     36 $self->{require_partial}=$params{require_partial} && 1;
108 13   100     40 $self->{require_length}=$params{require_length} && 1;
109 13   100     44 $self->{require_resource}=$params{require_resource} && 1;
110 13         27 $self->{methods}=['init'];
111 13         24 $self->{written}=0;
112 13         33 return $self;
113             }
114              
115             =head1 METHODS
116              
117             =over
118              
119             =item my $callback=$saver->get_callback();
120              
121             Returns a closure suitable for passing as the callback function argument
122             to L's request methods.
123              
124             =cut
125              
126             sub get_callback
127             {
128 0     0 1 0 my __PACKAGE__ $self=shift(@_);
129              
130             return sub
131             {
132 0     0   0 $self->process(@_);
133 0         0 };
134             }
135              
136             =item $saver->process($data,$response,$protocol);
137              
138             =item $saver->process(@_); # if called directly from the callback function
139              
140             Call this method from your callback function if you want to do more than
141             just save the incoming data (e.g. display a progress indicator).
142              
143             =cut
144              
145             sub process
146             {
147 219     219 1 1505 my __PACKAGE__ $self=shift(@_);
148              
149 219         296 for my $data (shift(@_)) {
150 219         225 my($len,$methods);
151              
152 219         338 $len=length($data);
153 219         276 $methods=$self->{methods};
154 219         444 for (my $off=0; $off<$len; ) {
155 241         224 my($method);
156              
157 241         281 $method=$methods->[-1];
158 241 100       377 if ($off) {
159 13         40 $off+=$self->$method(substr($data,$off),@_);
160             } else {
161 228         467 $off+=$self->$method($data,@_);
162             }
163             }
164             }
165             }
166              
167             =item $saver->get_length();
168              
169             Returns the total length of the returned entity, or an undefined value
170             if the length is indefinite (or hasn't arrived yet).
171              
172             =cut
173              
174             sub get_length
175             {
176 7     7 1 956 my __PACKAGE__ $self=shift(@_);
177 7         8 my($length);
178              
179 7         12 $length=$self->{length};
180 7 50 33     39 undef $length if defined($length) && $length eq '*';
181 7         27 return $length;
182             }
183              
184             =item $saver->get_type();
185              
186             Returns the MIME type of the returned entity, from either the
187             Content-Type header of the response or the first part header of a
188             multipart response. Returns undef if this information hasn't arrived
189             yet.
190              
191             =cut
192              
193             sub get_type
194             {
195 8     8 1 482 my __PACKAGE__ $self=shift(@_);
196              
197 8         31 return $self->{type};
198             }
199              
200             =item $saver->get_written();
201              
202             Returns the total number of bytes written by the saver (so far). Useful
203             for displaying a simple progress indicator.
204              
205             =cut
206              
207             sub get_written
208             {
209 9     9 1 506 my __PACKAGE__ $self=shift(@_);
210              
211 9         36 return $self->{written};
212             }
213              
214             =item $saver->get_ranges();
215              
216             Returns a reference to an array of ranges written by the saver (so far).
217             Each range is represented by a reference to a two-element array containing
218             the first and last byte numbers (ignoring the delta parameter) with the
219             same semantics as in the HTTP protocol. Useful for displaying a complex
220             progress indicator.
221              
222             =cut
223              
224             sub get_ranges
225             {
226 9     9 1 448 my __PACKAGE__ $self=shift(@_);
227              
228 9 100       10 return [map([@{$_}],grep($_->[1]>=$_->[0],@{$self->{ranges} || []}))];
  11         40  
  9         48  
229             }
230              
231             =item $saver->get_partheaders();
232              
233             Returns a reference to an array of HTTP::Headers objects, one for each
234             part (seen so far) of a multipart response.
235              
236             =cut
237              
238             sub get_partheaders
239             {
240 0     0 1 0 my __PACKAGE__ $self=shift(@_);
241              
242 0 0       0 return [@{$self->{partheaders} || []}];
  0         0  
243             }
244              
245             =item $saver->is_incomplete();
246              
247             Returns true if the saver hasn't seen a complete response yet.
248              
249             =cut
250              
251             sub is_incomplete
252             {
253 8     8 1 347 my __PACKAGE__ $self=shift(@_);
254 8         9 my($method);
255              
256 8         14 $method=$self->{methods}->[-1];
257 8   100     43 return $method ne 'ignore'
258             && $method ne 'indefinite';
259             }
260              
261             =back
262              
263             =cut
264              
265             sub ignore
266             {
267 10     10 0 12 my __PACKAGE__ $self=shift(@_);
268              
269 10         18 for my $data (shift(@_)) {
270 10         11 my($resp)=@_;
271              
272 10         30 $resp->add_content($data);
273 10         195 return length($data);
274             }
275             }
276              
277             sub indefinite
278             {
279 1     1 0 2 my __PACKAGE__ $self=shift(@_);
280              
281 1         3 for my $data (shift(@_)) {
282 1         1 my($len);
283 1         3 local($\);
284              
285 1         2 $len=length($data);
286 1 50       2 print {$self->{fh}} $data
  1         28  
287             or die "print error: $!";
288 1         2 $self->{written}+=$len;
289 1         3 $self->{ranges}->[-1]->[1]+=$len;
290 1         7 return $len;
291             }
292             }
293              
294             sub definite
295             {
296 28     28 0 33 my __PACKAGE__ $self=shift(@_);
297              
298 28         45 for my $data (shift(@_)) {
299 28         28 my($len,$expected);
300 28         59 local($\);
301              
302 28         37 $len=length($data);
303 28         43 $expected=$self->{expected};
304 28 100       48 if ($len>$expected) {
305 4         6 $len=$expected;
306 4         5 print {$self->{fh}} substr($data,0,$len);
  4         16  
307             } else {
308 24         27 print {$self->{fh}} $data;
  24         58  
309             }
310 28         54 $self->{ranges}->[-1]->[1]+=$len;
311 28         30 $self->{written}+=$len;
312 28         34 $expected-=$len;
313 28         35 $self->{expected}=$expected;
314 28 100       50 if (!$expected) {
315 7         8 pop(@{$self->{methods}});
  7         13  
316             }
317 28         149 return $len;
318             }
319             }
320              
321             sub headers
322             {
323 122     122 0 136 my __PACKAGE__ $self=shift(@_);
324              
325 122         150 for my $data (shift(@_)) {
326 122         183 for my $buffer ($self->{buffer}) {
327 122         112 my($len,$buflen,@lines,$headers,$content_range);
328              
329 122         148 $len=length($data);
330 122         116 $buflen=length($buffer);
331 122         185 $buffer.=$data;
332 122 100       797 $buffer =~ /\x0D?\x0A\x0D?\x0A/
333             or return $len;
334 6         14 $len=$+[0]-$buflen;
335 6         22 substr($buffer,$-[0])='';
336             # why is there no HTTP::Headers::parse anyway?
337 6         22 $buffer =~ s/\x0D?\x0A\s+/ /g;
338 6         30 @lines=split(/\x0D?\x0A/,$buffer);
339 6         9 $buffer='';
340 6         11 $self->{methods}->[-1]='boundary';
341 6         26 $headers=new HTTP::Headers;
342 6         48 foreach my $line (@lines) {
343 12         134 my($name,$value);
344              
345 12         53 $line =~ s/\s+$//;
346 12 50       74 ($name,$value)=($line =~ /^([^\s:]+)\s*:\s*(.*)$/)
347             or die "Malformed part headers";
348 12         41 $headers->push_header($name,$value);
349             }
350 6         108 push(@{$self->{partheaders}},$headers);
  6         14  
351 6         18 $content_range=$headers->header('Content-Range');
352 6 50       160 defined($content_range)
353             or die "Content-Range missing from part headers";
354 6         14 $self->content_range($content_range);
355 6 100       22 if (!defined($self->{type})) {
356 3         8 $self->{type}=$headers->header('Content-Type');
357             }
358 6         110 return $len;
359             }
360             }
361             }
362              
363             sub boundary
364             {
365 67     67 0 99 my __PACKAGE__ $self=shift(@_);
366              
367 67         87 for my $data (shift(@_)) {
368 67         102 for my $buffer ($self->{buffer}) {
369 67         71 my($len,$buflen,$pos,$methods);
370              
371 67         78 $methods=$self->{methods};
372 67         82 $len=length($data);
373 67         64 $buflen=length($buffer);
374 67         92 $buffer.=$data;
375 67 100       361 if ($buffer =~ $self->{start_boundary}) {
    100          
376 6         16 $len=$+[0]-$buflen;
377 6         12 $buffer='';
378 6         10 $methods->[-1]='headers';
379             } elsif ($buffer =~ $self->{end_boundary}) {
380 2         5 $len=$+[0]-$buflen;
381 2         3 $buffer='';
382 2         4 pop(@{$methods});
  2         3  
383             }
384 67         1303 return $len;
385             }
386             }
387             }
388              
389             sub init
390             {
391 13     13 0 15 my __PACKAGE__ $self=shift(@_);
392 13         19 my(undef,$resp)=@_;
393 13         13 my($code,$fh,$delta,$methods,$content_type);
394              
395 13         17 $methods=$self->{methods};
396 13         24 $methods->[-1]='ignore';
397 13         16 $fh=$self->{fh};
398 13         16 $delta=$self->{delta};
399 13         34 $code=$resp->code();
400 13         134 $content_type=$resp->header('Content-Type');
401 13 100 66     523 if ($code==206) {
    100          
402 6 100       17 if (defined(my $content_range=$resp->header('Content-Range'))) {
    50          
403 3         105 $self->content_range($content_range);
404 2         5 $self->{type}=$content_type;
405             } elsif (defined($content_type)) {
406 3         101 my($split,$ct,%params,$boundary);
407              
408 3         11 ($split)=split_header_words($content_type);
409 3         168 ($ct,undef,%params)=@{$split};
  3         11  
410 3 50 33     21 unless ($ct eq 'multipart/byteranges'
411             && defined($boundary=$params{boundary})) {
412 0         0 die "Unsupported Content-Type header";
413             }
414 3         7 undef $self->{type};
415 3         4 push(@{$methods},'boundary');
  3         6  
416 3         7 $self->{buffer}='';
417 3         38 $self->{start_boundary}=qr/\x0D?\x0A--\Q$boundary\E\x0D?\x0A/;
418 3         27 $self->{end_boundary}=qr/\x0D?\x0A--\Q$boundary\E--\x0D?\x0A/;
419 3         14 $self->{partheaders}=[];
420             } else {
421 0         0 die "Unsupported kind of partial content";
422             }
423             } elsif ($code==200 || $code==203) {
424 5 100       14 if ($self->{require_partial}) {
425 1         14 die "No partial content returned";
426             }
427 4 100       11 if (defined(my $content_length=$resp->header('Content-Length'))) {
428 2         66 $self->{length}=$content_length;
429 2 50       8 if ($self->{truncate}) {
430 0 0       0 truncate($fh,$content_length+$delta)
431             or die "truncate error: $!";
432             }
433 2         3 $self->{expected}=$content_length;
434 2 50       6 if ($content_length>0) {
435 2         2 push(@{$methods},'definite');
  2         5  
436             }
437             } else {
438 2 100       69 if ($self->{require_length}) {
439 1         9 die "No length returned";
440             }
441 1         3 $methods->[-1]='indefinite';
442             }
443 3 50       9 seek($fh,$delta,0)
444             or die "seek error: $!";
445 3 50       9 if ($methods->[-1] ne 'ignore') {
446 3         3 push(@{$self->{ranges}},[0,-1]);
  3         10  
447             }
448 3         7 $self->{type}=$content_type;
449             } else {
450 2 100       7 if ($self->{require_resource}) {
451 1         8 die "No resource returned";
452             }
453             }
454 9         58 return 0;
455             }
456              
457             my $content_range_re=qr#^\s*bytes\s+(\d+)-(\d+)/(\d+|\*)#;
458              
459             sub content_range
460             {
461 9     9 0 13 my __PACKAGE__ $self=shift(@_);
462 9         12 my($content_range)=@_;
463 9         12 my($first,$last,$length,$fh,$delta);
464              
465 9 50 33     152 unless (($first,$last,$length)=($content_range =~ $content_range_re)
      66        
      33        
466             and $last>=$first
467             and $length eq '*' || $last<$length) {
468 0         0 die "Malformed Content-Range header ($content_range)";
469             }
470 9         22 $fh=$self->{fh};
471 9         16 $delta=$self->{delta};
472 9 100       23 if (!defined($self->{length})) {
473 6         12 $self->{length}=$length;
474 6 100       14 if ($length eq '*') {
475 1 50       5 if ($self->{require_length}) {
476 1         8 die "No length returned";
477             }
478             } else {
479 5 50       13 if ($self->{truncate}) {
480 0 0       0 truncate($fh,$length+$delta)
481             or die "truncate error: $!";
482             }
483             }
484             }
485 8 50       22 seek($fh,$first+$delta,0)
486             or die "seek error: $!";
487 8         16 $self->{expected}=$last-$first+1;
488 8         10 push(@{$self->{methods}},'definite');
  8         17  
489 8         11 push(@{$self->{ranges}},[$first,$first-1]);
  8         39  
490             }
491              
492             1;
493              
494             =head1 AUTHOR
495              
496             Bo Lindbergh Eblgl@stacken.kth.seE
497              
498             =head1 COPYRIGHT AND LICENSE
499              
500             Copyright 2006 by Bo Lindbergh
501              
502             This library is free software; you can redistribute it and/or modify it
503             under the same terms as Perl itself, either Perl version 5.8.8 or, at
504             your option, any later version of Perl 5 you may have available.
505              
506             =cut
507