File Coverage

blib/lib/CPAN/Common/Index/Mirror.pm
Criterion Covered Total %
statement 140 141 99.2
branch 61 76 80.2
condition 6 6 100.0
subroutine 26 26 100.0
pod 4 7 57.1
total 237 256 92.5


line stmt bran cond sub pod time code
1 4     4   5000 use 5.008001;
  4         19  
2 4     4   26 use strict;
  4         12  
  4         118  
3 4     4   28 use warnings;
  4         11  
  4         249  
4              
5             package CPAN::Common::Index::Mirror;
6             # ABSTRACT: Search index via CPAN mirror flatfiles
7              
8             our $VERSION = '0.010';
9              
10 4     4   29 use parent 'CPAN::Common::Index';
  4         10  
  4         36  
11              
12 4     4   244 use Class::Tiny qw/cache mirror/;
  4         12  
  4         19  
13              
14 4     4   1445 use Carp;
  4         11  
  4         330  
15 4     4   2253 use CPAN::DistnameInfo;
  4         4253  
  4         140  
16 4     4   34 use File::Basename ();
  4         11  
  4         92  
17 4     4   6867 use File::Fetch;
  4         317800  
  4         189  
18 4     4   42 use File::Temp 0.19; # newdir
  4         122  
  4         443  
19 4     4   2684 use Search::Dict 1.07;
  4         4552  
  4         223  
20 4     4   1743 use Tie::Handle::SkipHeader;
  4         15284  
  4         153  
21 4     4   2318 use URI;
  4         19043  
  4         2766  
22              
23             our $HAS_IO_UNCOMPRESS_GUNZIP = eval { require IO::Uncompress::Gunzip };
24              
25             #pod =attr mirror
26             #pod
27             #pod URI to a CPAN mirror. Defaults to C.
28             #pod
29             #pod =attr cache
30             #pod
31             #pod Path to a local directory to store copies of the source indices. Defaults to a
32             #pod temporary directory if not specified.
33             #pod
34             #pod =cut
35              
36             sub BUILD {
37 30     30 0 133111 my $self = shift;
38              
39             # cache directory needs to exist
40 30         863 my $cache = $self->cache;
41 30 100       275 $cache = File::Temp->newdir
42             unless defined $cache;
43 30 50       2283 if ( !-d $cache ) {
44 0         0 Carp::croak("Cache directory '$cache' does not exist");
45             }
46 30         1615 $self->cache($cache);
47              
48             # ensure mirror URL ends in '/'
49 30         702 my $mirror = $self->mirror;
50 30 100       248 $mirror = "http://www.cpan.org/"
51             unless defined $mirror;
52 30         185 $mirror =~ s{/?$}{/};
53 30         552 $self->mirror($mirror);
54              
55 30         205 return;
56             }
57              
58             my %INDICES = (
59             mailrc => 'authors/01mailrc.txt.gz',
60             packages => 'modules/02packages.details.txt.gz',
61             );
62              
63             # XXX refactor out from subs below
64             my %TEST_GENERATORS = (
65             regexp_nocase => sub {
66             my $arg = shift;
67             my $re = ref $arg eq 'Regexp' ? $arg : qr/\A\Q$arg\E\z/i;
68             return sub { $_[0] =~ $re };
69             },
70             regexp => sub {
71             my $arg = shift;
72             my $re = ref $arg eq 'Regexp' ? $arg : qr/\A\Q$arg\E\z/;
73             return sub { $_[0] =~ $re };
74             },
75             version => sub {
76             my $arg = shift;
77             my $v = version->parse($arg);
78             return sub {
79             eval { version->parse( $_[0] ) == $v };
80             };
81             },
82             );
83              
84             my %QUERY_TYPES = (
85             # package search
86             package => 'regexp',
87             version => 'version',
88             dist => 'regexp',
89              
90             # author search
91             id => 'regexp_nocase', # XXX need to add "alias " first
92             fullname => 'regexp_nocase',
93             email => 'regexp_nocase',
94             );
95              
96             sub cached_package {
97 31     31 0 2411 my ($self) = @_;
98             my $package = File::Spec->catfile( $self->cache,
99 31         1212 File::Basename::basename( $INDICES{packages} ) );
100 31         3105 $package =~ s/\.gz$//;
101 31 100       880 $self->refresh_index unless -r $package;
102 31         183 return $package;
103             }
104              
105             sub cached_mailrc {
106 10     10 0 38 my ($self) = @_;
107             my $mailrc =
108 10         403 File::Spec->catfile( $self->cache, File::Basename::basename( $INDICES{mailrc} ) );
109 10         935 $mailrc =~ s/\.gz$//;
110 10 100       266 $self->refresh_index unless -r $mailrc;
111 10         52 return $mailrc;
112             }
113              
114             sub refresh_index {
115 12     12 1 2727 my ($self) = @_;
116 12         61 for my $file ( values %INDICES ) {
117 24         1183519 my $remote = URI->new_abs( $file, $self->mirror );
118 24 100       23454 $remote =~ s/\.gz$//
119             unless $HAS_IO_UNCOMPRESS_GUNZIP;
120 24         545 my $ff = File::Fetch->new( uri => $remote );
121 24 50       166011 my $where = $ff->fetch( to => $self->cache )
122             or Carp::croak( $ff->error );
123 24 100       400077 if ($HAS_IO_UNCOMPRESS_GUNZIP) {
124 14         190 ( my $uncompressed = $where ) =~ s/\.gz$//;
125 4     4   51 no warnings 'once';
  4         12  
  4         4840  
126 14 50       153 IO::Uncompress::Gunzip::gunzip( $where, $uncompressed )
127             or Carp::croak "gunzip failed: $IO::Uncompress::Gunzip::GunzipError\n";
128             }
129             }
130 12         6422891 return 1;
131             }
132              
133             # epoch secs
134             sub index_age {
135 4     4 1 3038 my ($self) = @_;
136 4         19 my $package = $self->cached_package;
137 4 50       110 return ( -r $package ? ( stat($package) )[9] : 0 ); # mtime if readable
138             }
139              
140             sub search_packages {
141 51     51 1 40423 my ( $self, $args ) = @_;
142 51 50       220 Carp::croak("Argument to search_packages must be hash reference")
143             unless ref $args eq 'HASH';
144              
145 51         220 my $index_path = $self->cached_package;
146 51 50       781 die "Can't read $index_path" unless -r $index_path;
147              
148 51         477 my $fh = IO::Handle->new;
149 51 50       2221 tie *$fh, 'Tie::Handle::SkipHeader', "<", $index_path
150             or die "Can't tie $index_path: $!";
151              
152             # Convert scalars or regexps to subs
153 51         8242 my $rules;
154 51         402 while ( my ( $k, $v ) = each %$args ) {
155 56         287 $rules->{$k} = _rulify( $k, $v );
156             }
157              
158 51         156 my @found;
159 51 100 100     337 if ( $args->{package} and ref $args->{package} eq '' ) {
160             # binary search 02packages on package
161 36         614 my $pos = look $fh, $args->{package}, { xfrm => \&_xform_package, fold => 1 };
162 36 50       5205 return if $pos == -1;
163             # loop over any case-insensitive matching lines
164 36         147 LINE: while ( my $line = <$fh> ) {
165 76 100       1653 last unless $line =~ /\A\Q$args->{package}\E\s+/i;
166 42         199 push @found, _match_package_line( $line, $rules );
167             }
168             }
169             else {
170             # iterate all lines looking for match
171 15         90 LINE: while ( my $line = <$fh> ) {
172 778635         4886008 push @found, _match_package_line( $line, $rules );
173             }
174             }
175 51 100       1531 return wantarray ? @found : $found[0];
176             }
177              
178             sub search_authors {
179 10     10 1 16308 my ( $self, $args ) = @_;
180 10 50       106 Carp::croak("Argument to search_authors must be hash reference")
181             unless ref $args eq 'HASH';
182              
183 10         48 my $index_path = $self->cached_mailrc;
184 10 50       163 die "Can't read $index_path" unless -r $index_path;
185 10 50       348 open my $fh, $index_path or die "Can't open $index_path: $!";
186              
187             # Convert scalars or regexps to subs
188 10         40 my $rules;
189 10         88 while ( my ( $k, $v ) = each %$args ) {
190 10         40 $rules->{$k} = _rulify( $k, $v );
191             }
192              
193 10         22 my @found;
194 10 100 100     76 if ( $args->{id} and ref $args->{id} eq '' ) {
195             # binary search mailrec on package
196 4         46 my $pos = look $fh, $args->{id}, { xfrm => \&_xform_mailrc, fold => 1 };
197 4 50       100 return if $pos == -1;
198 4         42 my $line = <$fh>;
199 4         22 push @found, _match_mailrc_line( $line, $rules );
200             }
201             else {
202             # iterate all lines looking for match
203 6         84 LINE: while ( my $line = <$fh> ) {
204 31722         69512 push @found, _match_mailrc_line( $line, $rules );
205             }
206             }
207 10 100       243 return wantarray ? @found : $found[0];
208             }
209              
210             sub _rulify {
211 66     66   210 my ( $key, $arg ) = @_;
212 66 50       336 return $arg if ref($arg) eq 'CODE';
213 66         345 return $TEST_GENERATORS{ $QUERY_TYPES{$key} }->($arg);
214             }
215              
216             sub _xform_package {
217 1092     1092   58266 my @fields = split " ", $_[0], 2;
218 1092         3485 return $fields[0];
219             }
220              
221             sub _xform_mailrc {
222 52     52   1443 my @fields = split " ", $_[0], 3;
223 52         152 return $fields[1];
224             }
225              
226             sub _match_package_line {
227 778677     778677   1400972 my ( $line, $rules ) = @_;
228 778677 50       1389086 return unless defined $line;
229 778677         2196939 my ( $mod, $version, $dist, $comment ) = split " ", $line, 4;
230 778677 100       1715471 if ( $rules->{package} ) {
231 519132 100       868076 return unless $rules->{package}->($mod);
232             }
233 259616 100       466393 if ( $rules->{version} ) {
234 38 100       139 return unless $rules->{version}->($version);
235             }
236 259583 100       481283 if ( $rules->{dist} ) {
237 259545 100       461616 return unless $rules->{dist}->($dist);
238             }
239 43         279 $dist =~ s{\A./../}{};
240             return {
241 43         412 package => $mod,
242             version => $version,
243             uri => "cpan:///distfile/$dist",
244             };
245             }
246              
247             sub _match_mailrc_line {
248 31726     31726   63408 my ( $line, $rules ) = @_;
249 31726 50       60948 return unless defined $line;
250 31726         131461 my ( $id, $address ) = $line =~ m{\Aalias\s+(\S+)\s+"(.*)"};
251 31726         135280 my ( $fullname, $email ) = $address =~ m{([^<]+)<([^>]+)>};
252 31726         148002 $fullname =~ s/\s*$//;
253 31726 100       79365 if ( $rules->{id} ) {
254 10578 100       22094 return unless $rules->{id}->($id);
255             }
256 21154 100       39846 if ( $rules->{fullname} ) {
257 10574 100       19879 return unless $rules->{fullname}->($fullname);
258             }
259 10582 100       20312 if ( $rules->{email} ) {
260 10574 100       19480 return unless $rules->{email}->($email);
261             }
262             return {
263 10         99 id => $id,
264             fullname => $fullname,
265             email => $email,
266             };
267             }
268              
269             1;
270              
271              
272             # vim: ts=4 sts=4 sw=4 et:
273              
274             __END__