File Coverage

blib/lib/Plack/App/File/Range.pm
Criterion Covered Total %
statement 47 53 88.6
branch 20 34 58.8
condition 5 7 71.4
subroutine 6 7 85.7
pod 0 2 0.0
total 78 103 75.7


line stmt bran cond sub pod time code
1             package Plack::App::File::Range;
2 1     1   18 use 5.008001;
  1         3  
  1         39  
3 1     1   6 use strict;
  1         2  
  1         31  
4 1     1   13 use warnings;
  1         2  
  1         29  
5 1     1   6 use parent 'Plack::App::File';
  1         2  
  1         9  
6              
7             sub serve_path {
8 8     8 0 1363 my ($self, $env, $file) = @_;
9 8 100       39 my $range = $env->{HTTP_RANGE}
10             or return $self->SUPER::serve_path($env, $file);
11              
12 7 50       46 $range =~ s/^bytes=//
13             or return $self->return_416;
14              
15 7 50       37 my @ranges = split(/\s*,\s*/, $range)
16             or return $self->return_416;
17              
18 7   50     32 my $content_type = $self->content_type || Plack::MIME->mime_type($file) || 'text/plain';
19 7 50       234 if ($content_type =~ m!^text/!) {
20 7   50     32 $content_type .= "; charset=" . ($self->encoding || "utf-8");
21             }
22              
23 7         656 my @stat = stat $file;
24 7         17 my $len = $stat[7];
25              
26 7 100       22 if (@ranges == 1) {
27 6 50       23 my ($start, $end) = $self->_parse_range($range, $len)
28             or return $self->return_416;
29              
30 6         3112 require PerlIO::subfile;
31 0 0       0 open my $fh, "<:raw:subfile(start=$start,end=".($end+1).")", $file
32             or return $self->return_403;
33              
34 0         0 Plack::Util::set_io_path($fh, Cwd::realpath($file));
35              
36             return [
37 0         0 206,
38             [
39             'Content-Type' => $content_type,
40             'Content-Range' => "bytes $start-$end/$len",
41             'Last-Modified' => HTTP::Date::time2str( $stat[9] )
42             ],
43             $fh,
44             ];
45             }
46              
47             # Multiple ranges:
48             # http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2
49 1 50       67 open my $fh, "<:raw", $file
50             or return $self->return_403;
51              
52 1         11 require HTTP::Message;
53 1         10 my $msg = HTTP::Message->new([
54             'Content-Type' => 'multipart/byteranges',
55             'Last-Modified' => HTTP::Date::time2str( $stat[9] ),
56             ]);
57 1         129 my $buf = '';
58 1         3 for my $range (@ranges) {
59 2 50       2448 my ($start, $end) = $self->_parse_range($range, $len)
60             or return $self->return_416;
61              
62 2         13 sysseek $fh, $start, 0;
63 2         34 sysread $fh, $buf, ($end-$start+1);
64              
65 2         21 $msg->add_part(HTTP::Message->new(
66             ['Content-Type' => $content_type, 'Content-Range' => "bytes $start-$end/$len"],
67             $buf,
68             ));
69             }
70              
71 1         371 my $headers = $msg->headers;
72             return [
73 2         97 206,
74 1         285 [map { ($_ => scalar $headers->header($_)) } $headers->header_field_names],
75             [$msg->content],
76             ];
77             }
78              
79             sub _parse_range {
80 8     8   17 my ($self, $range, $len) = @_;
81              
82 8 50       199 $range =~ /^(\d*)-(\d*)$/ or return;
83              
84 8         25 my ($start, $end) = ($1, $2);
85              
86 8 100 100     59 if (length $start and length $end) {
    100          
    50          
87 4 50       16 return if $start > $end; # "200-100"
88 4 50       11 return if $end >= $len; # "0-0" on a 0-length file
89 4         23 return ($start, $end);
90             }
91             elsif (length $start) {
92 1 50       6 return if $start >= $len; # "0-" on a 0-length file
93 1         6 return ($start, $len-1);
94             }
95             elsif (length $end) {
96 3 50       11 return if $end > $len; # "-1" on a 0-length file
97 3         14 return ($len-$end, $len-1);
98             }
99              
100 0           return;
101             }
102              
103             sub return_416 {
104 0     0 0   my $self = shift;
105 0           return [416, ['Content-Type' => 'text/plain', 'Content-Length' => 29], ['Request Range Not Satisfiable']];
106             }
107              
108             1;
109              
110             __END__