File Coverage

blib/lib/AnyPAN/ReverseProxy.pm
Criterion Covered Total %
statement 45 115 39.1
branch 0 18 0.0
condition 0 9 0.0
subroutine 15 26 57.6
pod 3 6 50.0
total 63 174 36.2


line stmt bran cond sub pod time code
1             package AnyPAN::ReverseProxy;
2 1     1   6188 use strict;
  1         2  
  1         29  
3 1     1   6 use warnings;
  1         2  
  1         26  
4              
5 1     1   6 use parent qw/Plack::Component/;
  1         2  
  1         5  
6              
7 1     1   5211 use Plack::Util::Accessor qw/storage logger source_cache/;
  1         285  
  1         6  
8              
9 1     1   570 use Plack::Request;
  1         46947  
  1         59  
10 1     1   476 use Plack::Response;
  1         1233  
  1         30  
11 1     1   499 use Plack::MIME;
  1         859  
  1         46  
12 1     1   8 use Path::Tiny;
  1         2  
  1         71  
13              
14 1     1   6 use AnyPAN;
  1         2  
  1         53  
15 1     1   29 use AnyPAN::Merger;
  1         2  
  1         21  
16 1     1   5 use AnyPAN::Source;
  1         2  
  1         16  
17 1     1   6 use AnyPAN::SourceCache;
  1         1  
  1         38  
18 1     1   517 use AnyPAN::RetryPolicy::NoRetry;
  1         7  
  1         24  
19 1     1   6 use AnyPAN::PackageInfo;
  1         2  
  1         16  
20 1     1   14 use AnyPAN::Logger::Stderr;
  1         2  
  1         1352  
21              
22             our $DEFAULT_LOGGER = AnyPAN::Logger::Stderr->new(level => $ENV{ANYPAN_REVERSE_PROXY_LOG_LEVEL} || 'warn');
23             our $DEFAULT_REQUEST_TIMEOUT = 10;
24             our $DEFAULT_RETRY_POLICY = AnyPAN::RetryPolicy::NoRetry->instance();
25              
26             sub new {
27 0     0 1   my $class = shift;
28 0           my $self = $class->SUPER::new(@_);
29 0           $self->{sources} = [];
30              
31             # set default
32 0   0       $self->{logger} ||= $DEFAULT_LOGGER;
33 0   0       $self->{source_cache} ||= _get_default_source_cache($self->{logger});
34              
35 0           return $self;
36             }
37              
38             sub prepare_app {
39 0     0 1   my $self = shift;
40 0 0         unless ($self->storage) {
41 0           die "storage is required";
42             }
43              
44 0 0         unless (@{ $self->{sources} }) {
  0            
45 0           die "sources are required";
46             }
47             }
48              
49             sub add_source {
50 0     0 0   my ($self, $source_url) = @_;
51 0           my $source = AnyPAN::Source->new($source_url);
52 0           $self->logger->debug("add @{[ $source->name ]} as source");
  0            
53 0           push @{ $self->{sources} } => $source;
  0            
54             }
55              
56             sub call {
57 0     0 1   my ($self, $env) = @_;
58 0           my $req = Plack::Request->new($env);
59              
60             # GET/HEAD request only
61 0 0 0       unless ($req->method eq 'GET' || $req->method eq 'HEAD') {
62 0           return $self->_res_405()->finalize();
63             }
64              
65             # path routing
66 0 0         if ($req->path =~ m!^/authors/id/([A-Z])/(\1[A-Z])/\2[-A-Z0-9]*/.+$!o) {
    0          
67 0           return $self->proxt_to_storage_or_sources($req)->finalize();
68             } elsif ($req->path eq '/modules/02packages.details.txt.gz') {
69 0           return $self->proxy_to_storage($req)->finalize();
70             }
71              
72 0           return $self->_res_404()->finalize();
73             }
74              
75             sub proxt_to_storage_or_sources {
76 0     0 0   my ($self, $req) = @_;
77              
78             # try proxy to storage
79 0           my $res = $self->proxy_to_storage($req);
80 0 0         return $res if $res->status == 200;
81              
82             # remove "/authors/id/" (e.g. D/DU/DUMMY/Foo.tar.gz)
83 0           my $path = substr $req->path, length "/authors/id/";
84 0           my $package_info = AnyPAN::PackageInfo->new(path => $path);
85 0           my $content_type = Plack::MIME->mime_type($path);
86 0           for my $source (@{ $self->{sources} }) {
  0            
87             # fetch from source
88 0           my $package_path = eval {
89 0           $self->source_cache->get_or_fetch_package($source, $package_info)
90             };
91 0 0         if (my $e = $@) {
92 0 0         if (AnyPAN::Agent::Exception::NotFound->caught($e)) {
93 0           $self->logger->debug("skip package $path on @{[ $source->name ]}");
  0            
94 0           next; # skip it
95             }
96 0           $self->logger->error("failed to fetch package $path on @{[ $source->name ]}");
  0            
97 0           die $e;
98             }
99              
100 0           $self->logger->debug("found @{[ $req->path ]} from @{[ $source->name ]}");
  0            
  0            
101              
102             # save to storage
103 0           my $save_key = $source->package_path($package_info->canonicalized_path);
104 0           $self->storage->copy($package_path, $save_key);
105              
106             # create response
107 0           my $fh = $package_path->openr_raw();
108 0           my $res = Plack::Response->new(200, [
109             'Content-Type' => $content_type,
110             'Content-Length' => $package_path->stat->size,
111             'Cache-Control' => 'private',
112             ], $fh);
113 0           return $res;
114             }
115              
116 0           return $self->_res_404();
117             }
118              
119             sub proxy_to_storage {
120 0     0 0   my ($self, $req) = @_;
121 0           my $storage_key = substr $req->path, 1; # remove first slash
122 0           my $content_type = Plack::MIME->mime_type($storage_key);
123              
124             # check from storage
125 0           my $storage_path = $self->storage->fetch($storage_key);
126 0 0         return $self->_res_404() unless $storage_path;
127              
128             # create response
129 0           $self->logger->debug("found $storage_key from storage");
130 0           my $fh = $storage_path->openr_raw();
131 0           my $res = Plack::Response->new(200, [
132             'Content-Type' => $content_type,
133             'Content-Length' => $storage_path->stat->size,
134             'Cache-Control' => 'private',
135             ], $fh);
136 0           return $res;
137             }
138              
139 0     0     sub _res_404 { shift->_res_simple(404, 'Not Found') }
140 0     0     sub _res_405 { shift->_res_simple(405, 'Method Not Allowed') }
141              
142             sub _res_simple {
143 0     0     my ($self, $status, $content) = @_;
144 0           return Plack::Response->new($status, [
145             'Content-Type' => 'text/plain',
146             'Content-Length' => length $content,
147             'Cache-Control' => 'no-cache,no-store',
148             ], $content);
149             }
150              
151             sub _get_default_source_cache {
152 0     0     my $logger = shift;
153 0           return AnyPAN::SourceCache->new(
154             cache_dir => $AnyPAN::Merger::DEFAULT_SOURCE_CACHE_DIR,
155             index_cache_timeout => $AnyPAN::Merger::DEFAULT_SOURCE_INDEX_CACHE_TIMEOUT,
156             agent => _get_default_agent($logger),
157             logger => $logger,
158             );
159             }
160              
161             sub _get_default_agent {
162 0     0     my $logger = shift;
163 0           return AnyPAN::Agent->new(
164             agent => __PACKAGE__."/$AnyPAN::VERSION",
165             timeout => $DEFAULT_REQUEST_TIMEOUT,
166             logger => $logger,
167             retry_policy => $DEFAULT_RETRY_POLICY,
168             );
169             }
170              
171             1;
172             __END__