File Coverage

blib/lib/Plack/Middleware/Mirror.pm
Criterion Covered Total %
statement 71 71 100.0
branch 30 38 78.9
condition 5 7 71.4
subroutine 16 16 100.0
pod 2 4 50.0
total 124 136 91.1


line stmt bran cond sub pod time code
1             # vim: set ts=2 sts=2 sw=2 expandtab smarttab:
2             #
3             # This file is part of Plack-Middleware-Mirror
4             #
5             # This software is copyright (c) 2011 by Randy Stauner.
6             #
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             #
10 3     3   313036 use strict;
  3         8  
  3         120  
11 3     3   17 use warnings;
  3         7  
  3         167  
12              
13             package Plack::Middleware::Mirror;
14             BEGIN {
15 3     3   70 $Plack::Middleware::Mirror::VERSION = '0.401';
16             }
17             BEGIN {
18 3     3   60 $Plack::Middleware::Mirror::AUTHORITY = 'cpan:RWSTAUNER';
19             }
20             # ABSTRACT: Save responses to disk to mirror a site
21              
22 3     3   22 use parent 'Plack::Middleware';
  3         7  
  3         24  
23 3     3   33775 use Plack::Util;
  3         8  
  3         69  
24 3     3   16 use Plack::Util::Accessor qw( path mirror_dir debug status_codes );
  3         6  
  3         13  
25 3     3   2151 use HTTP::Date ();
  3         11446  
  3         66  
26              
27 3     3   22 use File::Path ();
  3         5  
  3         45  
28 3     3   15 use File::Spec ();
  3         4  
  3         2652  
29              
30             sub call {
31 12     12 1 164414 my ($self, $env) = @_;
32              
33             # if we decide not to save fall through to wrapped app
34 12   66     50 return $self->_save_response($env) || $self->app->($env);
35             }
36              
37             # is there any sort of logger available?
38             sub debug_log {
39 22     22 0 231 my ($self, $message) = @_;
40 22 50       63 print STDERR ref($self) . " $message\n"
41             if $self->debug;
42             }
43              
44             sub prepare_app {
45 3     3 1 5000 my ($self) = @_;
46 3 100       22 $self->status_codes([200])
47             unless defined $self->status_codes;
48             }
49              
50             sub _save_response {
51 12     12   27 my ($self, $env) = @_;
52              
53             # this path matching stuff stolen straight from Plack::Middleware::Static
54 12 50       57 my $path_match = $self->path or return;
55 12         124 my $path = $env->{PATH_INFO};
56              
57 12         31 for ($path) {
58 12 100       100 my $matched = 'CODE' eq ref $path_match ? $path_match->($_) : $_ =~ $path_match;
59 12 100       111 return unless $matched;
60             }
61              
62             # TODO: should we use Cwd here?
63 11   50     45 my $dir = $self->mirror_dir || 'mirror';
64              
65 11         283 my $file = File::Spec->catfile($dir, split(/\//, $path));
66 11         731 my $fdir = File::Spec->catdir( (File::Spec->splitpath($file))[0, 1] ); # dirname()
67              
68 11         33 my $content = '';
69              
70 11 50       46 $self->debug_log("preparing to mirror: $path")
71             if $self->debug;
72              
73             # fall back to normal request, but intercept response and save it
74             return $self->response_cb(
75             $self->app->($env),
76             sub {
77 11     11   427 my ($res) = @_;
78              
79 11 100       43 return unless $self->should_mirror_status($res->[0]);
80              
81             # content filter
82             return sub {
83 18         406 my ($chunk) = @_;
84              
85             # end of content
86 18 100       50 if ( !defined $chunk ) {
87              
88 9 50       34 $self->debug_log("attempting to save: $path => $file")
89             if $self->debug;
90              
91             # if writing to the file fails, don't kill the request
92             # (we'll try again next time anyway)
93 9         841 local $@;
94 9         21 eval {
95 9 100       2456 File::Path::mkpath($fdir, 0, oct(777)) unless -d $fdir;
96              
97 9 50       1164 open(my $fh, '>', $file)
98             or die "Failed to open '$file': $!";
99 9         28 binmode($fh);
100 9 50       269 print $fh $content
101             or die "Failed to write to '$file': $!";
102             # explicitly close fh so we can set the mtime below
103 9 50       511 close($fh)
104             or die "Failed to close '$file': $!";
105              
106             # copy mtime to file if available
107 9 100       263 if ( my $lm = Plack::Util::header_get($$res[1], 'Last-Modified') ) {
108 4         124 $lm =~ s/;.*//; # strip off any extra (copied from HTTP::Headers)
109             # may return undef which we could pass to utime, but why bother?
110             # zero (epoch) may be unlikely but is possible
111 4 100       19 if ( defined(my $ts = HTTP::Date::str2time($lm)) ) {
112 3         297 utime( $ts, $ts, $file );
113             }
114             }
115             };
116 9 50       252 warn $@ if $@;
117             }
118             # if called multiple times, concatenate response
119             else {
120 9         19 $content .= $chunk;
121             }
122 18         83 return $chunk;
123             }
124 9         84 }
125 11         1070 );
126             }
127              
128             sub should_mirror_status {
129 22     22 0 2133 my ( $self, $res_code ) = @_;
130 22   100     72 my $codes = $self->status_codes || [ 200 ];
131              
132             # if codes is an empty arrayref don't restrict by code, just allow all
133 22 100       256 return 1 if ! @$codes;
134              
135             # if status code is one of the accepted codes, return true
136 19         38 foreach my $code ( @$codes ) {
137 21 100       88 return 1 if $res_code == $code;
138             }
139              
140             # if none of the above is true don't mirror
141 6 100       21 $self->debug_log("ignoring unwanted status ($res_code)")
142             if $self->debug;
143 6         181 return 0;
144             }
145              
146             1;
147              
148              
149             __END__