File Coverage

blib/lib/CPAN/Common/Index/Mirror.pm
Criterion Covered Total %
statement 138 139 99.2
branch 56 72 77.7
condition 6 6 100.0
subroutine 26 26 100.0
pod 4 7 57.1
total 230 250 92.0


line stmt bran cond sub pod time code
1 4     4   4617 use 5.008001;
  4         15  
2 4     4   29 use strict;
  4         8  
  4         76  
3 4     4   18 use warnings;
  4         8  
  4         184  
4              
5             package CPAN::Common::Index::Mirror;
6             # ABSTRACT: Search index via CPAN mirror flatfiles
7              
8             our $VERSION = '0.007';
9              
10 4     4   22 use parent 'CPAN::Common::Index';
  4         8  
  4         29  
11              
12 4     4   183 use Class::Tiny qw/cache mirror/;
  4         8  
  4         16  
13              
14 4     4   1078 use Carp;
  4         8  
  4         250  
15 4     4   1429 use CPAN::DistnameInfo;
  4         2810  
  4         97  
16 4     4   31 use File::Basename ();
  4         8  
  4         56  
17 4     4   1886 use File::Fetch;
  4         207418  
  4         166  
18 4     4   39 use File::Temp 0.19; # newdir
  4         105  
  4         353  
19 4     4   2337 use IO::Uncompress::Gunzip ();
  4         115125  
  4         130  
20 4     4   1865 use Search::Dict 1.07;
  4         2965  
  4         221  
21 4     4   1332 use Tie::Handle::SkipHeader;
  4         11769  
  4         119  
22 4     4   1660 use URI;
  4         14827  
  4         5254  
23              
24             #pod =attr mirror
25             #pod
26             #pod URI to a CPAN mirror. Defaults to C.
27             #pod
28             #pod =attr cache
29             #pod
30             #pod Path to a local directory to store copies of the source indices. Defaults to a
31             #pod temporary directory if not specified.
32             #pod
33             #pod =cut
34              
35             sub BUILD {
36 22     22 0 41552 my $self = shift;
37              
38             # cache directory needs to exist
39 22         671 my $cache = $self->cache;
40 22 100       239 $cache = File::Temp->newdir
41             unless defined $cache;
42 22 50       3028 if ( !-d $cache ) {
43 0         0 Carp::croak("Cache directory '$cache' does not exist");
44             }
45 22         862 $self->cache($cache);
46              
47             # ensure mirror URL ends in '/'
48 22         497 my $mirror = $self->mirror;
49 22 100       194 $mirror = "http://www.cpan.org/"
50             unless defined $mirror;
51 22         126 $mirror =~ s{/?$}{/};
52 22         694 $self->mirror($mirror);
53              
54 22         168 return;
55             }
56              
57             my %INDICES = (
58             mailrc => 'authors/01mailrc.txt.gz',
59             packages => 'modules/02packages.details.txt.gz',
60             );
61              
62             # XXX refactor out from subs below
63             my %TEST_GENERATORS = (
64             regexp_nocase => sub {
65             my $arg = shift;
66             my $re = ref $arg eq 'Regexp' ? $arg : qr/\A\Q$arg\E\z/i;
67             return sub { $_[0] =~ $re };
68             },
69             regexp => sub {
70             my $arg = shift;
71             my $re = ref $arg eq 'Regexp' ? $arg : qr/\A\Q$arg\E\z/;
72             return sub { $_[0] =~ $re };
73             },
74             version => sub {
75             my $arg = shift;
76             my $v = version->parse($arg);
77             return sub {
78             eval { version->parse( $_[0] ) == $v };
79             };
80             },
81             );
82              
83             my %QUERY_TYPES = (
84             # package search
85             package => 'regexp',
86             version => 'version',
87             dist => 'regexp',
88              
89             # author search
90             id => 'regexp_nocase', # XXX need to add "alias " first
91             fullname => 'regexp_nocase',
92             email => 'regexp_nocase',
93             );
94              
95             sub cached_package {
96 21     21 0 547 my ($self) = @_;
97             my $package = File::Spec->catfile( $self->cache,
98 21         477 File::Basename::basename( $INDICES{packages} ) );
99 21         1205 $package =~ s/\.gz$//;
100 21 100       380 $self->refresh_index unless -r $package;
101 21         69 return $package;
102             }
103              
104             sub cached_mailrc {
105 5     5 0 16 my ($self) = @_;
106             my $mailrc =
107 5         143 File::Spec->catfile( $self->cache, File::Basename::basename( $INDICES{mailrc} ) );
108 5         337 $mailrc =~ s/\.gz$//;
109 5 50       98 $self->refresh_index unless -r $mailrc;
110 5         47 return $mailrc;
111             }
112              
113             sub refresh_index {
114 2     2 1 2131 my ($self) = @_;
115 2         9 for my $file ( values %INDICES ) {
116 4         856692 my $remote = URI->new_abs( $file, $self->mirror );
117 4         10671 my $ff = File::Fetch->new( uri => $remote );
118 4 50       13933 my $where = $ff->fetch( to => $self->cache )
119             or Carp::croak( $ff->error );
120 4         195607 ( my $uncompressed = $where ) =~ s/\.gz$//;
121 4 50       48 IO::Uncompress::Gunzip::gunzip( $where, $uncompressed )
122             or Carp::croak "gunzip failed: $IO::Uncompress::Gunzip::GunzipError\n";
123             }
124 2         872422 return 1;
125             }
126              
127             # epoch secs
128             sub index_age {
129 2     2 1 939 my ($self) = @_;
130 2         10 my $package = $self->cached_package;
131 2 50       43 return ( -r $package ? ( stat($package) )[9] : 0 ); # mtime if readable
132             }
133              
134             sub search_packages {
135 35     35 1 12774 my ( $self, $args ) = @_;
136 35 50       137 Carp::croak("Argument to search_packages must be hash reference")
137             unless ref $args eq 'HASH';
138              
139 35         127 my $index_path = $self->cached_package;
140 35 50       366 die "Can't read $index_path" unless -r $index_path;
141              
142 35         243 my $fh = IO::Handle->new;
143 35 50       1117 tie *$fh, 'Tie::Handle::SkipHeader', "<", $index_path
144             or die "Can't tie $index_path: $!";
145              
146             # Convert scalars or regexps to subs
147 35         4154 my $rules;
148 35         209 while ( my ( $k, $v ) = each %$args ) {
149 38         118 $rules->{$k} = _rulify( $k, $v );
150             }
151              
152 35         78 my @found;
153 35 100 100     214 if ( $args->{package} and ref $args->{package} eq '' ) {
154             # binary search 02packages on package
155 26         137 my $pos = look $fh, $args->{package}, { xfrm => \&_xform_package, fold => 1 };
156 26 50       797 return if $pos == -1;
157             # loop over any case-insensitive matching lines
158 26         70 LINE: while ( my $line = <$fh> ) {
159 54 100       801 last unless $line =~ /\A\Q$args->{package}\E\s+/i;
160 30         105 push @found, _match_package_line( $line, $rules );
161             }
162             }
163             else {
164             # iterate all lines looking for match
165 9         42 LINE: while ( my $line = <$fh> ) {
166 775095         4670747 push @found, _match_package_line( $line, $rules );
167             }
168             }
169 35 100       865 return wantarray ? @found : $found[0];
170             }
171              
172             sub search_authors {
173 5     5 1 4081 my ( $self, $args ) = @_;
174 5 50       23 Carp::croak("Argument to search_authors must be hash reference")
175             unless ref $args eq 'HASH';
176              
177 5         18 my $index_path = $self->cached_mailrc;
178 5 50       44 die "Can't read $index_path" unless -r $index_path;
179 5 50       129 open my $fh, $index_path or die "Can't open $index_path: $!";
180              
181             # Convert scalars or regexps to subs
182 5         11 my $rules;
183 5         27 while ( my ( $k, $v ) = each %$args ) {
184 5         17 $rules->{$k} = _rulify( $k, $v );
185             }
186              
187 5         9 my @found;
188 5 100 100     28 if ( $args->{id} and ref $args->{id} eq '' ) {
189             # binary search mailrec on package
190 2         14 my $pos = look $fh, $args->{id}, { xfrm => \&_xform_mailrc, fold => 1 };
191 2 50       35 return if $pos == -1;
192 2         18 my $line = <$fh>;
193 2         8 push @found, _match_mailrc_line( $line, $rules );
194             }
195             else {
196             # iterate all lines looking for match
197 3         35 LINE: while ( my $line = <$fh> ) {
198 31686         64106 push @found, _match_mailrc_line( $line, $rules );
199             }
200             }
201 5 100       145 return wantarray ? @found : $found[0];
202             }
203              
204             sub _rulify {
205 43     43   103 my ( $key, $arg ) = @_;
206 43 50       159 return $arg if ref($arg) eq 'CODE';
207 43         175 return $TEST_GENERATORS{ $QUERY_TYPES{$key} }->($arg);
208             }
209              
210             sub _xform_package {
211 778     778   25138 my @fields = split " ", $_[0], 2;
212 778         1686 return $fields[0];
213             }
214              
215             sub _xform_mailrc {
216 48     48   884 my @fields = split " ", $_[0], 3;
217 48         118 return $fields[1];
218             }
219              
220             sub _match_package_line {
221 775125     775125   1342319 my ( $line, $rules ) = @_;
222 775125 50       1526820 return unless defined $line;
223 775125         1957111 my ( $mod, $version, $dist, $comment ) = split " ", $line, 4;
224 775125 100       1787096 if ( $rules->{package} ) {
225 516760 100       920341 return unless $rules->{package}->($mod);
226             }
227 258423 100       507615 if ( $rules->{version} ) {
228 35 100       164 return unless $rules->{version}->($version);
229             }
230 258391 100       513724 if ( $rules->{dist} ) {
231 258365 100       469158 return unless $rules->{dist}->($dist);
232             }
233 29         129 $dist =~ s{\A./../}{};
234             return {
235 29         253 package => $mod,
236             version => $version,
237             uri => "cpan:///distfile/$dist",
238             };
239             }
240              
241             sub _match_mailrc_line {
242 31688     31688   56767 my ( $line, $rules ) = @_;
243 31688 50       62051 return unless defined $line;
244 31688         101375 my ( $id, $address ) = $line =~ m{\Aalias\s+(\S+)\s+"(.*)"};
245 31688         106967 my ( $fullname, $email ) = $address =~ m{([^<]+)<([^>]+)>};
246 31688         108441 $fullname =~ s/\s*$//;
247 31688 100       73935 if ( $rules->{id} ) {
248 10564 100       19090 return unless $rules->{id}->($id);
249             }
250 21127 100       43391 if ( $rules->{fullname} ) {
251 10562 100       17996 return unless $rules->{fullname}->($fullname);
252             }
253 10566 100       21373 if ( $rules->{email} ) {
254 10562 100       19625 return unless $rules->{email}->($email);
255             }
256             return {
257 5         33 id => $id,
258             fullname => $fullname,
259             email => $email,
260             };
261             }
262              
263             1;
264              
265              
266             # vim: ts=4 sts=4 sw=4 et:
267              
268             __END__