File Coverage

lib/CPAN/Source.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package CPAN::Source;
2 10     10   67567 use warnings;
  10         149  
  10         384  
3 10     10   54 use strict;
  10         20  
  10         626  
4 10     10   74 use feature qw(say);
  10         16  
  10         5457  
5 10     10   10302 use Try::Tiny;
  10         20947  
  10         698  
6 10     10   28892 use URI;
  10         73651  
  10         416  
7 10     10   9793 use Mouse;
  10         415300  
  10         1334  
8 10     10   22873 use Compress::Zlib;
  10         972253  
  10         3340  
9 10     10   13003 use LWP::UserAgent;
  10         787648  
  10         436  
10 10     10   10106 use XML::Simple qw(XMLin);
  0            
  0            
11             use Cache::File;
12             use DateTime;
13             use DateTime::Format::HTTP;
14             use CPAN::DistnameInfo;
15             use YAML::XS;
16             use JSON::XS;
17              
18             use CPAN::Source::Dist;
19             use CPAN::Source::Package;
20              
21             use constant { DEBUG => $ENV{DEBUG} };
22              
23             our $VERSION = '0.04';
24              
25              
26             # options ...
27              
28             has cache_path =>
29             is => 'rw',
30             isa => 'Str';
31              
32             has cache_expiry =>
33             is => 'rw';
34              
35             has cache =>
36             is => 'rw';
37              
38             has mirror =>
39             is => 'rw',
40             isa => 'Str';
41              
42             has source_mirror =>
43             is => 'rw',
44             isa => 'Str',
45             default => sub { 'http://cpansearch.perl.org/' };
46              
47              
48             # data accessors
49             has authors =>
50             is => 'rw',
51             isa => 'HashRef';
52              
53              
54             # dist info from CPAN::DistnameInfo
55             has dists =>
56             is => 'rw',
57             isa => 'HashRef',
58             default => sub { +{ } };
59              
60             has packages =>
61             is => 'rw',
62             isa => 'HashRef',
63             default => sub { +{ } };
64              
65             has packagelist_meta =>
66             is => 'rw',
67             isa => 'HashRef';
68              
69             has modlist =>
70             is => 'rw',
71             isa => 'HashRef';
72              
73             has mailrc =>
74             is => 'rw',
75             isa => 'HashRef';
76              
77             has stamp =>
78             is => 'rw',
79             lazy => 1,
80             default => sub {
81             my $self = shift;
82             my $content = $self->fetch_stamp;
83             my ( $ts , $date ) = split /\s/,$content;
84             return DateTime->from_epoch( epoch => $ts );
85             };
86              
87             has mirrors =>
88             is => 'rw',
89             isa => 'HashRef',
90             lazy => 1,
91             default => sub {
92             my $self = shift;
93             return unless $self->mirror;
94             # get 07mirror.json
95             my $json = $self->fetch_mirrors;
96             my $data = decode_json( $json );
97             return $data;
98             };
99              
100             sub debug {
101             say "[DEBUG] " ,@_ if DEBUG;
102             }
103              
104             sub BUILD {
105             my ($self,$args) = @_;
106             if( $args->{ cache_path } ) {
107             my $cache = Cache::File->new(
108             cache_root => $args->{cache_path},
109             default_expires => $args->{cache_expiry} || '3 minutes' );
110             $self->cache( $cache );
111             }
112              
113             $|++ if DEBUG;
114             }
115              
116             sub prepare {
117             my ($self) = @_;
118             $self->prepare_authors;
119             $self->prepare_mailrc;
120             $self->prepare_package_data;
121             $self->prepare_modlist;
122             }
123              
124             sub prepare_authors {
125             my $self = shift;
126              
127             debug "Prepare authors data...";
128              
129             my $authors = $self->fetch_whois;
130              
131             $self->authors( $authors );
132             return $authors;
133             }
134              
135             sub prepare_mailrc {
136             my $self = shift;
137             debug "Prepare mailrc data...";
138             my $mailrc_txt = $self->fetch_mailrc;
139             $self->mailrc( $self->parse_mailrc( $mailrc_txt ) );
140             }
141              
142             sub prepare_package_data {
143             my $self = shift;
144              
145             debug "Prepare pacakge data...";
146             $self->fetch_package_data;
147             return {
148             meta => $self->packagelist_meta,
149             packages => $self->packages,
150             };
151             }
152              
153              
154             sub prepare_modlist {
155             my $self = shift;
156             debug "Prepare modlist data...";
157             my $modlist_txt = _decode_gzip( $self->fetch_modlist_data );
158              
159             debug "Parsing modlist data...";
160             $self->modlist( $self->parse_modlist( $modlist_txt ) );
161             }
162              
163             sub recent {
164             my ($self,$period) = @_;
165             my $json = $self->fetch_recent( $period );
166             return decode_json( $json );
167             }
168              
169             sub parse_modlist {
170             my ($self,$modlist_data) = @_;
171              
172             debug "Building modlist data ...";
173              
174             my @lines = split(/\n/,$modlist_data);
175             splice @lines,0,10;
176             $modlist_data = join "\n", @lines;
177             eval $modlist_data;
178             return CPAN::Modulelist->data;
179             }
180              
181             sub parse_mailrc {
182             my ($self,$mailrc_txt) = @_;
183              
184             debug "Parsing mailrc ...";
185              
186             my @lines = split /\n/,$mailrc_txt;
187             my %result;
188             for ( @lines ) {
189             my ($abbr,$name,$email) = ( $_ =~ m{^alias\s+(.*?)\s+"(.*?)\s*<(.*?)>"} );
190             $result{ $abbr } = { name => $name , email => $email };
191             }
192             return \%result;
193             }
194              
195              
196             sub purge_cache {
197             my $self = shift;
198             $self->cache->purge;
199             }
200              
201             sub fetch_stamp {
202             my $self = shift;
203             my $content = $self->http_get( $self->mirror . '/modules/02STAMP' );
204             return $content;
205             }
206              
207             sub fetch_mirrors {
208             my $self = shift;
209             return $self->http_get( $self->mirror . '/modules/07mirror.json' );
210             }
211              
212             sub fetch_mailrc {
213             my $self = shift;
214             my $gz = $self->http_get( $self->mirror . '/authors/01mailrc.txt.gz');
215             return _decode_gzip($gz);
216             }
217              
218             sub fetch_package_data {
219             my $self = shift;
220             my $gz = $self->http_get( $self->mirror . '/modules/02packages.details.txt.gz' );
221             my $content = _decode_gzip($gz);
222              
223             debug "Parsing package data...";
224              
225             my @lines = split /\n/,$content;
226              
227             # File: 02packages.details.txt
228             # URL: http://www.perl.com/CPAN/modules/02packages.details.txt
229             # Description: Package names found in directory $CPAN/authors/id/
230             # Columns: package name, version, path
231             # Intended-For: Automated fetch routines, namespace documentation.
232             # Written-By: PAUSE version 1.14
233             # Line-Count: 93553
234             # Last-Updated: Thu, 08 Sep 2011 13:38:39 GMT
235              
236             my $meta = { };
237              
238             # strip meta tags
239             my @meta_lines = splice @lines,0,9;
240             for( @meta_lines ) {
241             next unless $_;
242             my ($attr,$val) = m{^(.*?):\s*(.*?)$};
243             $meta->{$attr} = $val;
244              
245             debug "meta: $attr => $val ";
246             }
247              
248             $meta->{'URL'} = URI->new( $meta->{'URL'} );
249             $meta->{'Line-Count'} = int( $meta->{'Line-Count'} );
250             $meta->{'Last-Updated'} =
251             DateTime::Format::HTTP->parse_datetime( $meta->{'Last-Updated'} );
252              
253             my $packages = { };
254             my $cnt = 0;
255             my $size = scalar @lines;
256              
257             debug "Loading CPAN::DistnameInfo ...";
258              
259             local $|;
260              
261             for ( @lines ) {
262             my ($package_name,$version,$path) = split /\s+/;
263              
264             printf("\r[% 7d/%d] " , ++$cnt , $size ) if DEBUG;
265              
266             $version = undef if $version eq 'undef';
267              
268             my $tar_path = $self->mirror . '/authors/id/' . $path;
269             my $dist;
270              
271             # debug "Processing $package_name from $tar_path...";
272              
273             # Which parses informatino from dist path
274             my $d = CPAN::DistnameInfo->new( $tar_path );
275             if( $d->version ) {
276             # register "Foo-Bar" to dists hash...
277             $dist = $self->new_dist( $d , $package_name );
278             $self->dists->{ $dist->name } = $dist
279             unless $self->dists->{ $dist->name };
280             }
281              
282             # Moose::Foo => { ..... }
283             $self->packages->{ $package_name } = CPAN::Source::Package->new({
284             package => $package_name,
285             version => $version,
286             path => $tar_path,
287             dist => $dist,
288             });
289             }
290              
291             $self->packagelist_meta( $meta );
292             }
293              
294             sub fetch_modlist_data {
295             my $self = shift;
296             return $self->http_get( $self->mirror . '/modules/03modlist.data.gz' )
297             }
298              
299             sub fetch_whois {
300             my $self = shift;
301              
302             if( $self->cache ) {
303             my $c = $self->cache->get('json_00whois.xml');
304             return decode_json($c) if $c;
305             }
306              
307             my $xml = $self->http_get( $self->mirror . '/authors/00whois.xml');
308              
309             debug "Parsing authors data...";
310              
311             my $authors = XMLin( $xml )->{cpanid};
312              
313             # cache this with json
314             if( $self->cache ) {
315             $self->cache->set('json_00whois.xml', encode_json($authors) );
316             }
317             return $authors;
318             }
319              
320             sub fetch_module_rss {
321             my $self = shift;
322             my $rss_xml = $self->http_get( $self->mirror . '/modules/01modules.mtime.rss' );
323             return $rss_xml;
324             }
325              
326             sub fetch_recent {
327             my ($self,$period) = @_;
328             $period ||= '1d';
329              
330             # http://search.cpan.org/CPAN/RECENT-1M.json
331             # http://ftp.nara.wide.ad.jp/pub/CPAN/RECENT-1M.json
332             return $self->http_get( $self->mirror . '/RECENT-'. $period .'.json' );
333             }
334              
335             sub module_source_path {
336             my ($self,$d) = ($_[0], $_[1]);
337             return undef unless $d->distvname;
338             return ( $self->source_mirror . '/src/' . $d->cpanid . '/' . $d->distvname );
339             }
340              
341              
342             sub author {
343             my ($self,$pause_id) = @_;
344             return $self->authors->{ $pause_id };
345             }
346              
347             # return package obj
348             sub package {
349             my ($self,$pkgname) = @_;
350             return $self->packages->{ $pkgname };
351             }
352              
353             # return dist
354             sub dist {
355             my ($self,$distname) = @_;
356             $distname =~ s/::/-/g;
357             return $self->dists->{ $distname };
358             }
359              
360             sub http_get {
361             my ($self,$url,$cache_expiry) = @_;
362              
363             if( $self->cache ) {
364             my $c = $self->cache->get( $url );
365             return $c if $c;
366             }
367              
368             my $content;
369             if( -e $url ) {
370             debug "Reading file $url ...";
371             local $/;
372             open FH , '<' , $url;
373             $content = ;
374             close FH;
375             } else {
376             debug "Downloading $url ...";
377             my $ua = $self->new_ua;
378             my $resp = $ua->get($url);
379             $content = $resp->content;
380             }
381             $self->cache->set( $url , $content , $cache_expiry ) if $self->cache;
382             return $content;
383             }
384              
385              
386             sub new_dist {
387             my ($self,$d, $package_name) = @_;
388             my %props = $d->properties;
389             my $dist = CPAN::Source::Dist->new({
390             %props, # Hash
391             name => $props{dist}, # Dist-Name
392             version_name => $props{distvname},
393             package_name => $package_name,
394             source_path => $self->module_source_path($d),
395             _parent => $self,
396             });
397             return $dist;
398             }
399              
400             sub new_ua {
401             my $self = shift;
402             my $ua = LWP::UserAgent->new;
403             $ua->env_proxy;
404             return $ua;
405             }
406              
407             sub _decode_gzip {
408             return Compress::Zlib::memGunzip( $_[0] );
409             }
410              
411             1;
412             __END__