File Coverage

blib/lib/Wallflower.pm
Criterion Covered Total %
statement 96 96 100.0
branch 44 44 100.0
condition 13 14 92.8
subroutine 17 17 100.0
pod 3 3 100.0
total 173 174 99.4


line stmt bran cond sub pod time code
1             package Wallflower;
2             $Wallflower::VERSION = '1.013';
3 6     6   456000 use strict;
  6         53  
  6         183  
4 6     6   33 use warnings;
  6         9  
  6         154  
5              
6 6     6   2669 use Plack::Util ();
  6         53618  
  6         126  
7 6     6   46 use Path::Tiny ();
  6         13  
  6         123  
8 6     6   1093 use URI;
  6         9546  
  6         156  
9 6     6   2110 use HTTP::Date qw( time2str str2time);
  6         14973  
  6         366  
10 6     6   3128 use HTTP::Headers::Fast; # same as Plack::Response
  6         33236  
  6         218  
11 6     6   46 use Carp;
  6         14  
  6         443  
12              
13             # quick getters
14             for my $attr (qw( application destination env index url )) {
15 6     6   46 no strict 'refs';
  6         12  
  6         2287  
16 352     352   4760 *$attr = sub { $_[0]{$attr} };
17             }
18              
19             # create a new instance
20             sub new {
21 20     20 1 83630 my ( $class, %args ) = @_;
22 20         106 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       799 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         42700 my $path;
40 17 100 100     55 if ( $path = $self->url->path and $path ne '/' ) {
41 1         619 require Plack::App::URLMap;
42 1         1969 my $urlmap = Plack::App::URLMap->new;
43 1         16 $urlmap->mount( $path => $self->application );
44 1         68 $self->{application} = $urlmap->to_app;
45             }
46              
47 17         626 return $self;
48             }
49              
50             # url -> file converter
51             sub target {
52 43     43 1 6375 my ( $self, $uri ) = @_;
53              
54             # the URI must have a path
55 43 100       105 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         567 my @segments = $uri->path_segments;
59 40 100       1458 $segments[-1] = $self->index if $segments[-1] eq '';
60              
61             # generate target file name
62 40         99 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   44 no strict 'refs';
  6         13  
  6         5118  
68       2     *{"Wallflower::NULL::$_"} = sub { };
69             }
70              
71             sub _build_handle {
72 23     23   49 my ($file) = @_;
73              
74             # get a file to save the content in
75 23         85 my $dir = $file->parent;
76 23 100 50     1564 eval { $dir->mkpath; 1; } or die "$@\n"
  2         44  
  2         481  
77             if !-e $dir;
78 23 100       686 open my $fh, '> :raw', $file # no stinky crlf on Win32
79             or die "Can't open $file for writing: $!\n";
80              
81 20         1544 return $fh;
82             }
83              
84             # save the URL to a file
85             sub get {
86 35     35 1 32150 my ( $self, $uri ) = @_;
87 35 100       190 $uri = URI->new($uri) if !ref $uri;
88              
89             # absolute paths have the empty string as their first path_segment
90 35 100 100     1678 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         2203 %{ $self->env },
  34         90  
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         1669 my $target = $self->target($uri);
124 33 100       1533 $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       1220 $uri->scheme( $env->{'psgi.url_scheme'} ) if !$uri->scheme;
128 33 100       3138 $uri->host( $env->{SERVER_NAME} ) if !$uri->host;
129              
130             # get the content
131 33         2732 my ( $status, $headers, $file, $content ) = ( 500, [], '', '' );
132 33         90 my $res = Plack::Util::run_app( $self->application, $env );
133              
134 33 100       4569 if ( ref $res eq 'ARRAY' ) {
    100          
135 26         73 ( $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   28 my $response = shift;
142              
143             # delayed response
144 6         16 ( $status, $headers, $content ) = @$response;
145              
146             # streaming
147 6 100       17 if ( !defined $content ) {
148 3 100       14 return bless {}, 'Wallflower::NULL'
149             if $status ne '200'; # we don't care about the body
150 2         5 return _build_handle( $file = $target );
151             }
152 3         6 return;
153 6         33 } );
154             }
155 1         185 else { croak "Unknown response from application: $res"; }
156              
157             # save the content to a file
158 32 100       313 if ( $status eq '200' ) {
159 23   100     91 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       97 if ( ref $content eq 'ARRAY' ) {
    100          
    100          
    100          
166 15         174 print $fh @$content;
167             }
168             elsif ( ref $content eq 'GLOB' ) {
169 1         7 local $/ = \8192;
170 1         40 print {$fh} $_ while <$content>;
  1         26  
171 1         15 close $content;
172             }
173 4         53 elsif ( eval { $content->can('getline') } ) {
174 1         7 local $/ = \8192;
175 1         6 while ( defined( my $line = $content->getline ) ) {
176 3         18 print {$fh} $line;
  3         11  
177             }
178 1         8 $content->close;
179             }
180             elsif ( !defined $content ) { } # already streamed
181             else {
182 1         93 croak "Don't know how to handle body: $content";
183             }
184              
185             # finish
186 19         750 close $fh;
187              
188             # if the app sent Last-Modified, set the local file date to that
189 19 100       213 if ( my $last_modified = HTTP::Headers::Fast->new(@$headers)
190             ->header('Last-Modified') ) {
191 1         195 my $epoch = str2time( $last_modified );
192 1         98 utime $epoch, $epoch, $file;
193             }
194             }
195              
196 28         2389 return [ $status, $headers, $file ];
197             }
198              
199             1;
200              
201             __END__