File Coverage

blib/lib/Limper/SendFile.pm
Criterion Covered Total %
statement 14 49 28.5
branch 0 22 0.0
condition 0 9 0.0
subroutine 5 9 55.5
pod 0 4 0.0
total 19 93 20.4


line stmt bran cond sub pod time code
1             package Limper::SendFile;
2             $Limper::SendFile::VERSION = '0.005';
3 2     2   21044 use base 'Limper';
  2         3  
  2         1662  
4 2     2   64581 use 5.10.0;
  2         9  
5 2     2   20 use strict;
  2         4  
  2         40  
6 2     2   10 use warnings;
  2         5  
  2         78  
7              
8             package # newline because Dist::Zilla::Plugin::PkgVersion and PAUSE indexer
9             Limper;
10              
11 2     2   1637 use Time::Local 'timegm';
  2         3292  
  2         2053  
12              
13             push @Limper::EXPORT, qw/public send_file/;
14             push @Limper::EXPORT_OK, qw/mime_types parse_date/;
15              
16             my %mime_types = map { chomp; split /\t/; } ();
17              
18 0     0 0   sub mime_types { \%mime_types }
19              
20             my $public = './public/';
21              
22             sub public {
23 0 0   0 0   if (defined wantarray) { $public } else { ($public) = @_ }
  0            
  0            
24             }
25              
26             # parse whatever crappy date a client might give
27             my @months = qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/;
28             sub parse_date {
29 0     0 0   my ($d, $m, $y, $h, $n, $s) = $_[0] =~ qr/^(?:\w+), (\d\d)[ -](\w+)[ -](\d\d(?:\d\d)?) (\d\d):(\d\d):(\d\d) GMT$/;
30 0 0         ($m, $d, $h, $n, $s, $y) = $_[0] =~ qr/^(?:\w+) (\w+) ([ \d]\d) (\d\d):(\d\d):(\d\d) (\d{4})$/ unless defined $d;
31 0 0         return 0 unless defined $d;
32 0 0         timegm( $s, $n, $h, $d, (grep { $months[$_] eq $m } 0..$#months)[0], $y + (length $y == 2 ? 1900 : 0) );
  0            
33             }
34              
35             # support If-Modified-Since and If-Unmodified-Since
36             hook after => sub {
37             my ($request, $response) = @_;
38             if ($request->{method} // '' eq 'GET' and substr($response->{status} // 200, 0, 1) == 2 and exists $response->{headers}{'Last-Modified'}) {
39             for my $since (grep { /if-(?:un)?modified-since/ } keys %{$request->{headers}}) {
40             next if $since eq 'if-modified-since' and ($response->{status} // 200) != 200;
41             if (parse_date($request->{headers}{$since}) >= parse_date($response->{headers}{'Last-Modified'})) {
42             $response->{body} = '';
43             $response->{status} = $since eq 'if-modified-since' ? 304 : 412;
44             }
45             }
46             }
47             };
48              
49             sub send_file {
50 0   0 0 0   my $file = $_[0] // request->{uri};
51              
52 0           $file =~ s{^/}{$public/};
53 0 0         if ($file =~ qr{/\.\./}) {
54 0           status 403;
55 0           return 'Forbidden';
56             }
57 0 0 0       if (-e $file and -r $file) {
58 0 0         if (-f $file) {
    0          
59 0 0 0       if (!exists response->{headers}{'Content-Type'} and my ($ext) = $file =~ /\.(\w+)$/) {
60 0 0         headers 'Content-Type' => $mime_types{$ext} if exists $mime_types{$ext};
61             }
62 0           open my $fh, '<', $file;
63 0           headers 'Last-Modified' => rfc1123date((stat($fh))[9]);
64 0           join '', map { $_ } (<$fh>);
  0            
65             } elsif (-d $file) {
66 0           opendir(my $dh, $file);
67 0           my @files = sort grep { !/^\./ } readdir $dh;
  0            
68 0           my $path = request->{uri};
69 0 0         $path .= '/' unless $path =~ m|/$|;
70 0           @files = map { "$_
" } @files;
  0            
71 0           headers 'Content-Type' => 'text/html';
72 0           join "\n", 'Directory listing of ' . request->{uri} . '', @files, '';
73             } else {
74 0           status 500;
75 0           $Limper::reasons->{500};
76             }
77             } else {
78 0           status 404;
79 0           'This is the void';
80             }
81             }
82              
83             1;
84              
85             =for Pod::Coverage
86              
87             =head1 NAME
88              
89             Limper::SendFile - add static content support to Limper
90              
91             =head1 VERSION
92              
93             version 0.005
94              
95             =head1 SYNOPSIS
96              
97             # order is important:
98             use Limper::SendFile;
99             use Limper;
100              
101             # some other routes
102              
103             get qr{^/} => sub {
104             send_file; # sends request->{uri} by default
105             };
106              
107             limp;
108              
109             =head1 DESCRIPTION
110              
111             B extends L to also return actual files. Because sometimes that's needed.
112              
113             =head1 EXPORTS
114              
115             The following are all additionally exported by default:
116              
117             public send_file
118              
119             Also exportable:
120              
121             mime_types parse_date
122              
123             =head1 FUNCTIONS
124              
125             =head2 send_file
126              
127             Sends either the file name given, or the value of C<< request->{uri} >> if no file name given.
128              
129             The following as the last defined route will have B look for the file as a last resort:
130              
131             get qr{^/} => sub { send_file }
132              
133             B will be set by file extension if known and header has not already been defined.
134             Default is B.
135              
136             =head2 public
137              
138             Get or set the public root directory. Default is B<./public/>.
139              
140             my $public = public;
141              
142             public '/var/www/langlang.us/public_html';
143              
144             =head1 ADDITIONAL FUNCTIONS
145              
146             =head2 parse_date
147              
148             Liberally parses whatever date a client might give, returning a Unix timestamp.
149              
150             # these all return 784111777
151             my $date = parse_date("Sun, 06 Nov 1994 08:49:37 GMT");
152             my $date = parse_date("Sunday, 06-Nov-94 08:49:37 GMT");
153             my $date = parse_date("Sun Nov 6 08:49:37 1994");
154              
155             =head2 mime_types
156              
157             Returns a B of file extension / content-type pairs.
158              
159             =head1 HOOKS
160              
161             =head2 after
162              
163             An B hook is created to support B and B, comparing to B.
164             This runs for all defined routes, not just those using B.
165              
166             =head1 COPYRIGHT AND LICENSE
167              
168             Copyright (C) 2014 by Ashley Willis Eashley+perl@gitable.orgE
169              
170             This library is free software; you can redistribute it and/or modify
171             it under the same terms as Perl itself, either Perl version 5.12.4 or,
172             at your option, any later version of Perl 5 you may have available.
173              
174             =head1 SEE ALSO
175              
176             L
177              
178             L
179              
180             L
181              
182             =cut
183              
184             __DATA__