File Coverage

blib/lib/Wallflower.pm
Criterion Covered Total %
statement 97 97 100.0
branch 46 46 100.0
condition 13 14 92.8
subroutine 17 17 100.0
pod 3 3 100.0
total 176 177 99.4


line stmt bran cond sub pod time code
1             package Wallflower;
2             $Wallflower::VERSION = '1.014';
3 6     6   450076 use strict;
  6         54  
  6         191  
4 6     6   33 use warnings;
  6         11  
  6         157  
5              
6 6     6   2568 use Plack::Util ();
  6         52424  
  6         127  
7 6     6   43 use Path::Tiny ();
  6         11  
  6         106  
8 6     6   1051 use URI;
  6         9403  
  6         164  
9 6     6   2114 use HTTP::Date qw( time2str str2time);
  6         15030  
  6         365  
10 6     6   3276 use HTTP::Headers::Fast; # same as Plack::Response
  6         32721  
  6         218  
11 6     6   46 use Carp;
  6         12  
  6         460  
12              
13             # quick getters
14             for my $attr (qw( application destination env index url )) {
15 6     6   44 no strict 'refs';
  6         22  
  6         2335  
16 352     352   4596 *$attr = sub { $_[0]{$attr} };
17             }
18              
19             # create a new instance
20             sub new {
21 20     20 1 80078 my ( $class, %args ) = @_;
22 20         92 my $self = bless {
23             destination => Path::Tiny->new('.'),
24             env => {},
25             index => 'index.html',
26             url => 'http://localhost/',
27             %args,
28             }, $class;
29              
30             # some basic parameter checking
31 20 100       881 croak "application is required" if !defined $self->application;
32 19 100 100     53 croak "destination is invalid"
33             if !-e $self->destination || !-d $self->destination;
34              
35             # turn the url attribute into a URI object
36 17         329 $self->{url} = URI->new( $self->url );
37              
38             # if the application is mounted somewhere
39 17         41538 my $path;
40 17 100 100     49 if ( $path = $self->url->path and $path ne '/' ) {
41 1         582 require Plack::App::URLMap;
42 1         1889 my $urlmap = Plack::App::URLMap->new;
43 1         16 $urlmap->mount( $path => $self->application );
44 1         66 $self->{application} = $urlmap->to_app;
45             }
46              
47 17         647 return $self;
48             }
49              
50             # url -> file converter
51             sub target {
52 43     43 1 6326 my ( $self, $uri ) = @_;
53              
54             # the URI must have a path
55 43 100       124 croak "$uri has an empty path" if !length $uri->path;
56              
57             # URI ending with / have the empty string as their last path_segment
58 40         563 my @segments = $uri->path_segments;
59 40 100       1474 $segments[-1] = $self->index if $segments[-1] eq '';
60              
61             # generate target file name
62 40         94 return Path::Tiny->new( $self->destination, grep length, @segments );
63             }
64              
65             # Wallflower::NULL drops the streamed data
66             for (qw( write close )) {
67 6     6   47 no strict 'refs';
  6         12  
  6         5289  
68       2     *{"Wallflower::NULL::$_"} = sub { };
69             }
70              
71             sub _build_handle {
72 23     23   48 my ($file) = @_;
73              
74             # get a file to save the content in
75 23         82 my $dir = $file->parent;
76 23 100 50     1529 eval { $dir->mkpath; 1; } or die "$@\n"
  2         42  
  2         446  
77             if !-e $dir;
78 23 100       742 open my $fh, '> :raw', $file # no stinky crlf on Win32
79             or die "Can't open $file for writing: $!\n";
80              
81 20         1482 return $fh;
82             }
83              
84             # save the URL to a file
85             sub get {
86 35     35 1 32375 my ( $self, $uri ) = @_;
87 35 100       187 $uri = URI->new($uri) if !ref $uri;
88              
89             # absolute paths have the empty string as their first path_segment
90 35 100 100     1641 croak "$uri is not an absolute URI"
91             if $uri->path && length +( $uri->path_segments )[0];
92              
93             # setup the environment
94             my $env = {
95              
96             # current environment
97             %ENV,
98              
99             # overridable defaults
100             'psgi.errors' => \*STDERR,
101              
102             # current instance defaults
103 34         2147 %{ $self->env },
  34         91  
104             ('psgi.url_scheme' => $self->url->scheme )x!! $self->url->scheme,
105              
106             # request-related environment variables
107             REQUEST_METHOD => 'GET',
108              
109             # request attributes
110             SCRIPT_NAME => '',
111             PATH_INFO => $uri->path,
112             REQUEST_URI => $uri->path,
113             QUERY_STRING => '',
114             SERVER_NAME => $self->url->host,
115             SERVER_PORT => $self->url->port,
116             SERVER_PROTOCOL => "HTTP/1.0",
117              
118             # wallflower defaults
119             'psgi.streaming' => 1,
120             };
121              
122             # add If-Modified-Since headers if the target file exists
123 34         1650 my $target = $self->target($uri);
124 33 100       1478 $env->{HTTP_IF_MODIFIED_SINCE} = time2str( ( stat _ )[9] ) if -e $target;
125              
126             # fixup URI (needed to resolve relative URLs in retrieved documents)
127 33 100       1095 $uri->scheme( $env->{'psgi.url_scheme'} ) if !$uri->scheme;
128 33 100       3200 $uri->host( $env->{SERVER_NAME} ) if !$uri->host;
129              
130             # get the content
131 33         2708 my ( $status, $headers, $file, $content ) = ( 500, [], '', '' );
132 33         89 my $res = Plack::Util::run_app( $self->application, $env );
133              
134 33 100       4220 if ( ref $res eq 'ARRAY' ) {
    100          
135 26         78 ( $status, $headers, $content ) = @$res;
136             }
137             elsif ( ref $res eq 'CODE' ) {
138              
139             # https://metacpan.org/pod/PSGI#Delayed-Response-and-Streaming-Body
140             $res->( sub {
141 6     6   29 my $response = shift;
142              
143             # delayed response
144 6         14 ( $status, $headers, $content ) = @$response;
145              
146             # streaming
147 6 100       18 if ( !defined $content ) {
148 3 100       16 return bless {}, 'Wallflower::NULL'
149             if $status ne '200'; # we don't care about the body
150 2         7 return _build_handle( $file = $target );
151             }
152 3         5 return;
153 6         31 } );
154             }
155 1         110 else { croak "Unknown response from application: $res"; }
156              
157             # save the content to a file
158 32 100       281 if ( $status == 200 ) {
    100          
159 23   100     73 my $fh = defined $content && do {
160             eval { _build_handle( $file = $target ) }
161             or do { warn $@; return [ 999, [], '' ]; };
162             };
163              
164             # copy content to the file
165 20 100       99 if ( ref $content eq 'ARRAY' ) {
    100          
    100          
    100          
166 15         151 print $fh @$content;
167             }
168             elsif ( ref $content eq 'GLOB' ) {
169 1         6 local $/ = \8192;
170 1         36 print {$fh} $_ while <$content>;
  1         30  
171 1         14 close $content;
172             }
173 4         52 elsif ( eval { $content->can('getline') } ) {
174 1         6 local $/ = \8192;
175 1         5 while ( defined( my $line = $content->getline ) ) {
176 3         18 print {$fh} $line;
  3         12  
177             }
178 1         8 $content->close;
179             }
180             elsif ( !defined $content ) { } # already streamed
181             else {
182 1         82 croak "Don't know how to handle body: $content";
183             }
184              
185             # finish
186 19         694 close $fh;
187              
188             # if the app sent Last-Modified, set the local file date to that
189 19 100       198 if ( my $last_modified = HTTP::Headers::Fast->new(@$headers)
190             ->header('Last-Modified') ) {
191 1         134 my $epoch = str2time( $last_modified );
192 1         94 utime $epoch, $epoch, $file;
193             }
194             }
195             elsif ( $status == 304 ) {
196 1         3 $file = $target;
197             }
198              
199 28         2711 return [ $status, $headers, $file ];
200             }
201              
202             1;
203              
204             __END__