File Coverage

blib/lib/CPAN/Index/Loader.pm
Criterion Covered Total %
statement 84 93 90.3
branch 18 30 60.0
condition 2 6 33.3
subroutine 20 22 90.9
pod 9 9 100.0
total 133 160 83.1


line stmt bran cond sub pod time code
1             package CPAN::Index::Loader;
2              
3             =pod
4              
5             =head1 NAME
6              
7             CPAN::Index::Loader - Populates the CPAN index SQLite database
8              
9             =head1 DESCRIPTION
10              
11             This package implements all the functionality required to download
12             the CPAN index data, parse it, and populate the SQLite database
13             file.
14              
15             Because it involves loading a number of otherwise unneeded modules,
16             this package is B loaded by default with the rest of
17             L, but may be loaded on-demand if needed.
18              
19             =head1 METHODS
20              
21             =cut
22              
23 5     5   4565 use strict;
  5         11  
  5         198  
24 5     5   29 use Carp ();
  5         9  
  5         75  
25 5     5   2029 use IO::File ();
  5         42484  
  5         110  
26 5     5   8343 use IO::Zlib ();
  5         572312  
  5         161  
27 5     5   11100 use Params::Util qw{ _INSTANCE _HANDLE };
  5         13653  
  5         448  
28 5     5   43 use Email::Address ();
  5         105  
  5         71  
29 5     5   6217 use CPAN::Cache ();
  5         13836046  
  5         171  
30              
31 5     5   126 use vars qw{$VERSION};
  5         12  
  5         245  
32             BEGIN {
33 5     5   13881 $VERSION = '0.01';
34             }
35              
36              
37              
38              
39              
40             #####################################################################
41             # Constructor and Accessors
42              
43             =pod
44              
45             =head2 new
46              
47             my $loader = CPAN::Index::Loader->new(
48             remote_uri => 'http://search.cpan.org/CPAN',
49             local_dir => '/tmp/cpanindex',
50             );
51              
52             =cut
53              
54             sub new {
55 2     2 1 117622 my $class = shift;
56 2         16 my $self = bless { @_ }, $class;
57              
58             # Create the cache object
59 2 50       12 unless ( $self->cache ) {
60 2         15 my @params = ();
61 2         37 $self->{cache} = CPAN::Cache->new(
62             remote_uri => delete($self->{remote_uri}),
63             local_dir => delete($self->{local_dir}),
64             trace => $self->{trace},
65             verbose => $self->{verbose},
66             );
67             }
68              
69 2         34738 $self;
70             }
71              
72             =pod
73              
74             =head2 cache
75              
76             The C accessor returns a L object that represents the
77             CPAN cache.
78              
79             =cut
80              
81             sub cache {
82 8     8 1 65 $_[0]->{cache};
83             }
84              
85             =pod
86              
87             =head2 remote_uri
88              
89             The C accessor return a L object for the location of the
90             CPAN mirror.
91              
92             =cut
93              
94             sub remote_uri {
95 0     0 1 0 $_[0]->cache->remote_uri;
96             }
97              
98             =pod
99              
100             =head2 local_dir
101              
102             The C accessor returns the filesystem path for the root directory
103             of the local CPAN file cache.
104              
105             =cut
106              
107             sub local_dir {
108 0     0 1 0 $_[0]->cache->local_dir;
109             }
110              
111             =pod
112              
113             =head2 local_file
114              
115             my $path = $loader->local_file('01mailrc.txt.gz');
116              
117             The C method takes the name of a file in the CPAN and returns
118             the local path to the file.
119              
120             Returns a path string, or throws an exception on error.
121              
122             =cut
123              
124             sub local_file {
125 6     6 1 2353 $_[0]->cache->file($_[1])->path;
126             }
127              
128             =pod
129              
130             =head2 local_handle
131              
132             my $path = $loader->local_handle('01mailrc.txt.gz');
133              
134             The C method takes the name of a file in the CPAN and returns
135             an L to the file.
136              
137             Returns an L, most likely an L, or throws an
138             exception on error.
139              
140             =cut
141              
142             sub local_handle {
143 4     4 1 1366 my $self = shift;
144 4         19 my $file = $self->local_file(shift);
145 4 100       653 $file =~ /\.gz$/
146             ? IO::Zlib->new( $file, 'rb' ) # [r]ead [b]inary file
147             : IO::File->new( $file );
148             }
149              
150              
151              
152              
153              
154             #####################################################################
155             # Main Methods
156              
157             =pod
158              
159             =head2 load_index
160              
161             The C takes a single param of the schema to load, locates
162             the three main index files based on the C path, and then
163             loads the index from those files.
164              
165             Returns the total number of records added.
166              
167             =cut
168              
169             sub load_index {
170 1     1 1 790 my $self = shift;
171 1         4 my $schema = shift;
172 1         2 my $created = 0;
173              
174             # Load the files
175 1   33     6 $created += $self->load_authors(
176             $schema,
177             $self->local_handle('authors/01mailrc.txt') ||
178             $self->local_handle('authors/01mailrc.txt.gz'),
179             );
180 1   33     33 $created += $self->load_packages(
181             $schema,
182             $self->local_handle('modules/02packages.details.txt') ||
183             $self->local_handle('modules/02packages.details.txt.gz'),
184             );
185              
186             # Return the total
187 1         33 $created;
188             }
189              
190              
191              
192              
193              
194             #####################################################################
195             # Parsing Methods
196              
197             =pod
198              
199             =head2 load_authors
200              
201             CPAN::Index::Loader->load_authors( $schema, $handle );
202              
203             The C method populates the C table from the CPAN
204             F<01mailrc.txt.gz> file.
205              
206             The C table in the SQLite database should already be empty
207             B this method is called.
208              
209             Returns the number of authors added to the database, or throws an
210             exception on error.
211              
212             =cut
213              
214             sub load_authors {
215 2     2 1 201580 my $self = shift;
216 2 50       39 my $schema = _INSTANCE(shift, 'DBIx::Class::Schema')
217             or Carp::croak("Did not provide a DBIx::Class::Schema param");
218 2 50       93 my $handle = _HANDLE(shift)
219             or Carp::croak("Did not provide a file handle param");
220              
221             # Wrap the actual method in a DBIx::Class transaction
222 2         46 my $created = 0;
223            
224 2         12 my $rs = eval {
225             $schema->txn_do( sub {
226 2     2   33754 $created = $self->_load_authors( $schema, $handle );
227 2         51 } );
228             };
229 2 50       2335904 if ( $@ =~ /Rollback failed/ ) {
    50          
230 0         0 Carp::croak("Rollback failed, database may be corrupt");
231             } elsif ( $@ ) {
232 0         0 Carp::croak("Database error while loading authors: $@");
233             }
234              
235 2         213 $created;
236             }
237            
238             sub _load_authors {
239 2     2   8 my ($self, $schema, $handle) = @_;
240              
241             # Every email address should be different, so disable
242             # Email::Address caching so we don't waste a bunch of memory.
243 2         6 local $Email::Address::NOCACHE = 1;
244              
245             # Process the author records
246 2         6 my $created = 0;
247 2         88 while ( my $line = $handle->getline ) {
248             # Parse the line
249 18 50       1082 unless ( $line =~ /^alias\s+(\S+)\s+\"(.+)\"[\012\015]+$/ ) {
250 0         0 Carp::croak("Invalid 01mailrc.txt.gz line '$line'");
251             }
252 18         48 my $id = $1;
253 18         43 my $email = $2;
254              
255             # Parse the full email address to seperate the parts
256 18         129 my @found = Email::Address->parse($email);
257 18 50       12642 unless ( @found ) {
258             # Invalid email or something that Email::Address can't handle.
259             # Use a default name and address for now.
260 0         0 @found = Email::Address->parse( "$id <$id\@cpan.org>" );
261             }
262              
263             # Some CPAN users have multiple addresses, for example
264             # A. PREM ANAND
265             # When this happens, we'll just take the first one.
266              
267             # Create the record
268 18         121 $schema->resultset('Author')->create( {
269             id => $id,
270             name => $found[0]->name,
271             email => $found[0]->address,
272             } );
273 18         170745 $created++;
274              
275             # Debugging
276             #if ( $Test::More::VERSION ) {
277             # Test::More::diag("$created...");
278             #}
279             }
280              
281 2         215 $created;
282             }
283              
284             =pod
285              
286             =head2 load_packages
287              
288             CPAN::Index::Loader->load_packages( $schema, $handle );
289              
290             The C method populates the C table from the CPAN
291             F<02packages.details.txt.gz> file.
292              
293             The C table in the SQLite database should already be empty
294             B this method is called.
295              
296             Returns the number of packages added to the database, or throws an
297             exception on error.
298              
299             =cut
300              
301             sub load_packages {
302 2     2 1 1656 my $self = shift;
303 2 50       32 my $schema = _INSTANCE(shift, 'DBIx::Class::Schema')
304             or Carp::croak("Did not provide a DBIx::Class::Schema param");
305 2 50       85 my $handle = _HANDLE(shift)
306             or Carp::croak("Did not provide a file handle param");
307              
308             # Advance past the header, to the first blank line
309 2         90 while ( my $line = $handle->getline ) {
310 18 100       2204 last if $line !~ /[^\s\012\015]/;
311             }
312              
313             # Wrap the database method in a DBIx::Class transaction
314 2         238 my $created;
315 2         9 my $rs = eval {
316             $schema->txn_do( sub {
317 2     2   1373 $created = $self->_load_packages( $schema, $handle );
318 2         31 } );
319             };
320 2 50       334995 if ( $@ =~ /Rollback failed/ ) {
    50          
321 0         0 Carp::croak("Rollback failed, database may be corrupt");
322             } elsif ( $@ ) {
323 0         0 Carp::croak("Database error while loading packages: $@");
324             }
325              
326 2         15 $created;
327             }
328              
329             sub _load_packages {
330 2     2   7 my ($self, $schema, $handle) = @_;
331              
332             # Process the author records
333 2         5 my $created = 0;
334 2         73 while ( my $line = $handle->getline ) {
335 18 50       748 unless ( $line =~ /^(\S+)\s+(\S+)\s+(.+?)[\012\015]+$/ ) {
336 0         0 Carp::croak("Invalid 02packages.details.txt.gz line '$line'");
337             }
338 18         40 my $name = $1;
339 18 100       71 my $version = $2 eq 'undef' ? undef : $2;
340 18         337 my $path = $3;
341              
342             # Create the record
343 18         120 $schema->resultset('Package')->create( {
344             name => $name,
345             version => $version,
346             path => $path,
347             } );
348 18         85326 $created++;
349              
350             # Debugging
351             #if ( $Test::More::VERSION ) {
352             # Test::More::diag("$created...");
353             #}
354             }
355              
356 2         108 $created;
357             }
358              
359             1;
360              
361             =pod
362              
363             =head1 SUPPORT
364              
365             Bugs should be reported via the CPAN bug tracker
366              
367             L
368              
369             For other issues, contact the author.
370              
371             =head1 AUTHOR
372              
373             Adam Kennedy Ecpan@ali.asE
374              
375             Parts based on various modules by Leon Brocard Eacme@cpan.orgE
376              
377             =head1 SEE ALSO
378              
379             Related: L, L
380              
381             Based on: L, L
382              
383             =head1 COPYRIGHT
384              
385             Copyright (c) 2006 Adam Kennedy.
386              
387             This program is free software; you can redistribute
388             it and/or modify it under the same terms as Perl itself.
389              
390             The full text of the license can be found in the
391             LICENSE file included with this module.
392              
393             =cut