File Coverage

lib/WorePAN.pm
Criterion Covered Total %
statement 153 303 50.5
branch 34 144 23.6
condition 13 55 23.6
subroutine 24 41 58.5
pod 18 18 100.0
total 242 561 43.1


line stmt bran cond sub pod time code
1             package WorePAN;
2              
3 13     13   199200 use strict;
  13         19  
  13         339  
4 13     13   38 use warnings;
  13         13  
  13         248  
5 13     13   7456 use File::Temp ();
  13         199655  
  13         265  
6 13     13   5227 use Path::Extended::Tiny ();
  13         142983  
  13         234  
7 13     13   72 use File::Spec;
  13         13  
  13         197  
8 13     13   7474 use HTTP::Tiny;
  13         368492  
  13         24445  
9              
10             our $VERSION = '0.17';
11              
12             sub new {
13 1     1 1 3006 my ($class, %args) = @_;
14              
15 1 50       7 $args{verbose} = $ENV{TEST_VERBOSE} unless defined $args{verbose};
16              
17 1 50       4 if ($args{use_minicpan}) {
18 0 0       0 eval { require CPAN::Mini } or die "requires CPAN::Mini";
  0         0  
19 0         0 my %mini_config = CPAN::Mini->read_config;
20 0         0 my $local = $mini_config{local};
21 0 0 0     0 die "MiniCPAN not found" unless $local && -d $local;
22 0         0 $args{root} = $local;
23 0         0 $args{cleanup} = 0;
24             }
25              
26 1 50       6 if (!$args{root}) {
27 1 50       7 $args{root} = File::Temp::tempdir(CLEANUP => 1, ($args{tmp} ? (DIR => $args{tmp}) : TMPDIR => 1));
28 1 50       491 warn "'root' is missing; created a temporary WorePAN directory: $args{root}\n" if $args{verbose};
29             }
30 1         8 $args{root} = Path::Extended::Tiny->new($args{root})->mkdir;
31 1   50     40 $args{cpan} ||= "http://www.cpan.org/";
32 1 50       3 if ($args{use_backpan}) {
33 0   0     0 $args{backpan} ||= "http://backpan.cpan.org/";
34             }
35 1 50 33     6 $args{no_network} = 1 if !defined $args{no_network} && $ENV{HARNESS_ACTIVE};
36              
37 1         2 $args{pid} = $$;
38              
39 1   33     12 $args{ua} ||= HTTP::Tiny->new(
40             agent => "WorePAN/$VERSION",
41             );
42              
43 1         60 my $self = bless \%args, $class;
44              
45 1 50       1 my @files = @{ delete $self->{files} || [] };
  1         7  
46 1 50       2 if (!$self->{no_network}) {
47 0 0       0 if (my $dists = delete $self->{dists}) {
48 0         0 push @files, $self->_dists2files($dists);
49             }
50             }
51             # XXX: I don't think we need something like ->_mods2files, right?
52              
53 1 50       2 if (@files) {
54 1         4 $self->_fetch(\@files);
55 1         5236 $self->update_indices(%args);
56             }
57              
58 1         5 $self;
59             }
60              
61 0     0 1 0 sub root { shift->{root} }
62             sub file {
63 1     1 1 26 my $self = shift;
64 1 50       12 my $file = $self->_normalize(File::Spec->catfile(@_)) or return;
65 1         5 $self->{root}->file('authors/id', $file);
66             }
67 1     1 1 5 sub whois { shift->{root}->file('authors/00whois.xml') }
68 1     1 1 4 sub mailrc { shift->{root}->file('authors/01mailrc.txt.gz') }
69 1     1 1 3 sub packages_details { shift->{root}->file('modules/02packages.details.txt.gz') }
70              
71             sub slurp_whois {
72 0     0 1 0 my $self = shift;
73 0         0 my $index = $self->whois;
74 0         0 require Parse::CPAN::Whois;
75 0         0 Parse::CPAN::Whois->new($index->path)->authors;
76             }
77              
78             sub slurp_mailrc {
79 0     0 1 0 my $self = shift;
80 0         0 $self->_slurp($self->mailrc);
81             }
82             sub slurp_packages_details {
83 0     0 1 0 my $self = shift;
84 0         0 $self->_slurp($self->packages_details);
85             }
86              
87             sub _slurp {
88 0     0   0 my ($self, $index) = @_;
89 0 0       0 return unless $index->exists;
90              
91 0         0 require IO::Zlib;
92 0 0       0 my $fh = IO::Zlib->new($index->path, "rb") or die $!;
93 0         0 my @lines;
94             my $done_preambles;
95 0         0 while(<$fh>) {
96 0         0 chomp;
97 0 0       0 if (/^\s*$/) {
98 0         0 $done_preambles = 1;
99 0         0 next;
100             }
101 0 0       0 next unless $done_preambles;
102 0         0 push @lines, $_;
103             }
104 0         0 @lines;
105             }
106              
107             sub add_files {
108 0     0 1 0 my ($self, @files) = @_;
109 0         0 $self->_fetch(\@files);
110             }
111              
112             sub add_dists {
113 0     0 1 0 my ($self, %dists) = @_;
114 0 0       0 if ($self->{no_network}) {
115 0         0 warn "requires network\n";
116 0         0 return;
117             }
118 0         0 my @files = $self->_dists2files(\%dists);
119 0         0 $self->_fetch(\@files);
120             }
121              
122             sub _fetch {
123 1     1   2 my ($self, $files) = @_;
124              
125 1         1 my %authors;
126             my %packages;
127 1         4 my $_root = $self->{root}->subdir('authors/id');
128 1         58 for my $file (@$files) {
129 1         1 my $dest;
130 1 50 33     24 if (-f $file && $file =~ /\.(?:tar\.(?:gz|bz2)|tgz|zip)$/) {
131 1         3 my $source = Path::Extended::Tiny->new($file);
132 1         21 $dest = $_root->file('L/LO/LOCAL/', $source->basename);
133 1         70 $self->_log("copy $source to $dest");
134 1         4 $source->copy_to($dest);
135 1         6550 $dest->mtime($source->mtime);
136             }
137             else {
138 0 0       0 $file = $self->_normalize($file) or next;
139 0 0       0 $dest = $self->__fetch($file) or next;
140             }
141             }
142             }
143              
144             sub _normalize {
145 1     1   1 my ($self, $file) = @_;
146              
147 1 50       4 $file =~ s|\\|/|g if $^O eq 'MSWin32';
148              
149 1 50       5 if ($file !~ m{^([A-Z])/(\1[A-Z0-9_])/\2[A-Z0-9_\-]*/.+}) {
150 0 0       0 if ($file =~ m{^([A-Z])([A-Z0-9_])[A-Z0-9_\-]*/.+}) {
151 0         0 $file = "$1/$1$2/$file";
152             }
153             else {
154 0         0 warn "unsupported file format: $file\n";
155 0         0 return;
156             }
157             }
158 1         4 return $file;
159             }
160              
161             sub _dists2files {
162 0     0   0 my ($self, $dists) = @_;
163 0 0       0 return unless ref $dists eq ref {};
164              
165 0         0 require URI;
166 0         0 require URI::QueryParam;
167 0         0 require JSON::PP;
168              
169 0         0 my $uri = URI->new('http://api.cpanauthors.org/uploads/dist');
170 0         0 my @keys = keys %$dists;
171 0         0 my @files;
172 0         0 while (@keys) {
173 0         0 my @tmp = splice @keys, 0, 50;
174             $uri->query_param(d => [
175 0 0       0 map { $dists->{$_} ? "$_,$dists->{$_}" : $_ } @tmp
  0         0  
176             ]);
177 0         0 $self->_log("called API: $uri");
178 0         0 my $res = $self->{ua}->get($uri);
179 0 0       0 if (!$res->{success}) {
180 0         0 warn "API error: $uri $res->{status} $res->{reason}";
181 0         0 return;
182             }
183 0         0 my $rows = eval { JSON::PP::decode_json($res->{content}) };
  0         0  
184 0 0       0 if ($@) {
185 0         0 warn $@;
186 0         0 return;
187             }
188 0         0 push @files, @$rows;
189             }
190              
191             map {
192 0         0 $_->{filename} && $_->{author}
193             ? join '/',
194             substr($_->{author}, 0, 1),
195             substr($_->{author}, 0, 2),
196             $_->{author},
197             $_->{filename}
198             : ()
199 0 0 0     0 } @files;
200             }
201              
202             sub _log {
203 4     4   46 my ($self, $message) = @_;
204 4 50       26 print STDERR "$message\n" if $self->{verbose};
205             }
206              
207             sub __fetch {
208 0     0   0 my ($self, $file) = @_;
209              
210 0         0 my $dest = $self->{root}->file("authors/id/", $file);
211 0 0       0 return $dest if $dest->exists;
212              
213 0         0 $dest->parent->mkdir;
214              
215 0 0       0 if ($self->{local_mirror}) {
216 0         0 my $source = Path::Extended::Tiny->new($self->{local_mirror}, "authors/id", $file);
217 0 0       0 if ($source->exists) {
218 0         0 $self->_log("copy $source to $dest");
219 0         0 $source->copy_to($dest);
220 0         0 $dest->mtime($source->mtime);
221 0         0 return $dest;
222             }
223             }
224 0 0       0 if (!$self->{no_network}) {
225 0         0 my $url = $self->{cpan}."authors/id/$file";
226 0         0 $self->_log("mirror $url to $dest");
227 0         0 my $res = $self->{ua}->mirror($url => $dest);
228 0 0       0 return $dest if $res->{success};
229 0 0       0 if ($self->{backpan}) {
230 0         0 my $url = $self->{backpan}."authors/id/$file";
231 0         0 $self->_log("mirror $url to $dest");
232 0         0 my $res = $self->{ua}->mirror($url => $dest);
233 0 0       0 return $dest if $res->{success};
234             }
235             }
236 0         0 warn "Can't fetch $file\n";
237 0         0 return;
238             }
239              
240             sub walk {
241 1     1 1 2 my $self = shift;
242       0     my %args = (@_ == 1 && ref $_[0] eq ref sub {})
243 1 50 33     12 ? (callback => $_[0])
244             : @_;
245 1         8 my $root = $self->{root}->subdir('authors/id');
246 1   33     118 my $tmproot = $self->{tmp} || $args{tmp};
247 1   33     5 my $allow_dev_releases = $args{developer_releases} || $self->{developer_releases};
248              
249 1         388 require Archive::Any::Lite;
250              
251 1         40297 local $Archive::Any::Lite::IGNORE_SYMLINK = 1;
252             $root->recurse(callback => sub {
253 5     5   839 my $archive_file = shift;
254 5 100       16 return if -d $archive_file;
255              
256 1         19 my $path = $archive_file->relative($root);
257 1         192 my $basename = $archive_file->basename;
258 1 50       18 return unless $basename =~ /\.(?:tar\.(?:gz|bz2)|tgz|zip)$/;
259 1 50       3 return if $basename =~ /^perl\-\d+/; # perls
260 1 50 33     10 return if !$allow_dev_releases && (
      33        
261             $basename =~ /\d\.\d+_\d/ # dev release
262             or $basename =~ /TRIAL/ # trial release
263             );
264              
265 1         4 my $archive = Archive::Any::Lite->new($archive_file->path);
266 1 50       66 my $tmpdir = Path::Extended::Tiny->new(File::Temp::tempdir(CLEANUP => 1, ($tmproot ? (DIR => $tmproot) : (TMPDIR => 1))));
267 1         390 $archive->extract($tmpdir);
268 1 50       3160 my $basedir = $tmpdir->children == 1 ? ($tmpdir->children)[0] : $tmpdir;
269 1 50       253 $basedir = $tmpdir unless -d $basedir;
270              
271 1         17 $args{callback}->($basedir, $path, $archive_file);
272              
273 1         4 $tmpdir->remove;
274 1         12 });
275             }
276              
277             sub update_indices {
278 1     1 1 4 my ($self, %args) = @_;
279              
280 1 50       4 return if $self->{no_indices};
281              
282 1         7 require IO::Zlib;
283 1         508 require Parse::PMFile;
284 1         34511 require Parse::LocalDistribution;
285              
286 1   33     3495 my $allow_dev_releases = $args{developer_releases} || $self->{developer_releases};
287 1   33     6 my $permissions = $args{permissions} || $self->{permissions};
288              
289 1         2 my (%authors, %packages);
290             $self->walk(%args, callback => sub {
291 1     1   2 my ($basedir, $path, $archive_file) = @_;
292              
293 1         4 my $mtime = $archive_file->mtime;
294 1         159 my ($author) = $path =~ m{^[A-Z]/[A-Z][A-Z0-9_]/([^/]+)/};
295 1         7 $authors{$author} = 1;
296              
297             # a dist that has blib/ shouldn't be indexed
298             # see PAUSE::dist::mail_summary
299 1 50 33     4 return if $basedir->basename eq 'blib' or $basedir->subdir('blib')->exists;
300              
301 1         103 my $args = {ALLOW_DEV_VERSION => $allow_dev_releases};
302 1 50       9 if ($permissions) {
303 0         0 $args->{PERMISSIONS} = $permissions;
304 0         0 $args->{USERID} = $author;
305             }
306 1         8 my $parser = Parse::LocalDistribution->new($args);
307 1         15 my $info = $parser->parse($basedir);
308 1         4218 $self->_update_packages(\%packages, $info, $path, $mtime);
309 1         12 });
310 1         646 $self->_write_whois(\%authors);
311 1         8 $self->_write_mailrc(\%authors);
312 1         8 $self->_write_packages_details(\%packages);
313              
314 1         10 return 1;
315             }
316              
317             sub _update_packages {
318 1     1   1 my ($self, $packages, $info, $path, $mtime) = @_;
319              
320 1         4 for my $module (sort keys %$info) {
321 1 50       3 next unless exists $info->{$module}{version};
322 1         2 my $new_version = $info->{$module}{version};
323 1 50       2 if (!$packages->{$module}) { # shortcut
324 1         3 $packages->{$module} = [$new_version, $path, $mtime];
325 1         6 next;
326             }
327 0         0 my $ok = 0;
328 0         0 my $cur_version = $packages->{$module}[0];
329 0 0       0 if (Parse::PMFile->_vgt($new_version, $cur_version)) {
    0          
330 0         0 $ok++;
331             }
332             elsif (Parse::PMFile->_vgt($cur_version, $new_version)) {
333             # lower VERSION number
334             }
335             else {
336 13     13   80 no warnings; # numeric/version
  13         21  
  13         11721  
337 0 0 0     0 if (
      0        
338             $new_version eq 'undef' or $new_version == 0 or
339             Parse::PMFile->_vcmp($new_version, $cur_version) == 0
340             ) {
341 0 0       0 if ($mtime >= $packages->{$module}[2]) {
342 0         0 $ok++; # dist is newer
343             }
344             }
345             }
346 0 0       0 if ($ok) {
347 0         0 $packages->{$module} = [$new_version, $path, $mtime];
348             }
349             }
350             }
351              
352             sub _write_whois {
353 1     1   14 my ($self, $authors) = @_;
354              
355 1         3 my $index = $self->whois;
356 1         89 $index->parent->mkdir;
357 1         68 $index->openw;
358 1         101 $index->printf(qq{\n\n}, scalar(gmtime), $VERSION);
359 1         24 for my $id (sort keys %$authors) {
360 1         4 $index->printf("%sauthor%s%s\@cpan.org1\n", $id, $id, lc $id);
361             }
362 1         12 $index->print("\n");
363 1         10 $index->close;
364 1         32 $self->_log("created $index");
365             }
366              
367             sub _write_mailrc {
368 1     1   2 my ($self, $authors) = @_;
369              
370 1         3 my $index = $self->mailrc;
371 1         61 $index->parent->mkdir;
372 1 50       57 my $fh = IO::Zlib->new($index->path, "wb") or die $!;
373 1         1039 for my $id (sort keys %$authors) {
374 1         9 $fh->printf("alias %s \"%s <%s\@cpan.org>\"\n", $id, $id, lc $id);
375             }
376 1         120 $fh->close;
377 1         223 $self->_log("created $index");
378             }
379              
380             sub _write_packages_details {
381 1     1   2 my ($self, $packages) = @_;
382              
383 1         5 my $index = $self->packages_details;
384 1         118 $index->parent->mkdir;
385 1 50       155 my $fh = IO::Zlib->new($index->path, "wb") or die $!;
386 1         854 $fh->print("File: 02packages.details.txt\n");
387 1         126 $fh->print("Last-Updated: ".localtime(time)."\n");
388 1         69 $fh->print("\n");
389 1 0       57 for my $pkg (map {$_->[1]} sort {($a->[0] cmp $b->[0]) || ($a->[1] cmp $b->[1])} map {[lc $_, $_]} keys %$packages) {
  1         3  
  0         0  
  1         4  
390 1         2 my ($first, $second) = (30, 8);
391 1 50       4 my $ver = defined $packages->{$pkg}[0] ? $packages->{$pkg}[0] : 'undef';
392 1 50       3 if (length($pkg) > $first) {
393 0         0 $second = length($ver);
394 0         0 $first += 8 - $second;
395             }
396             $fh->printf("%-${first}s %${second}s %s\n",
397             $pkg,
398             $ver,
399 1         6 $packages->{$pkg}[1]
400             );
401             }
402 1         68 $fh->close;
403 1         179 $self->_log("created $index");
404             }
405              
406             sub look_for {
407 0     0 1 0 my ($self, $package) = @_;
408              
409 0 0       0 return unless defined $package;
410              
411 0         0 for ($self->slurp_packages_details) {
412 0 0       0 if (/^$package\s+(\S+)\s+(\S+)$/) {
413 0 0       0 return wantarray ? ($1, $2) : $1;
414             }
415             }
416 0         0 return;
417             }
418              
419 0     0 1 0 sub authors { shift->_authors_whois }
420              
421             sub _authors_mailrc {
422 0     0   0 my $self = shift;
423              
424 0         0 my @authors;
425 0         0 for ($self->slurp_mailrc) {
426 0         0 my ($id, $name, $email) = /^alias\s+(\S+)\s+"?(.+?)\s+(\S+?)"?\s*$/;
427 0 0       0 next unless $id;
428 0         0 $email =~ tr/<>//d;
429 0         0 push @authors, {pauseid => $id, name => $name, email => $email};
430             }
431 0         0 \@authors;
432             }
433              
434             sub _authors_whois {
435 0     0   0 my $self = shift;
436              
437 0         0 my @authors;
438 0         0 for ($self->slurp_whois) {
439 0         0 push @authors, {
440             pauseid => $_->pauseid,
441             name => $_->name,
442             asciiname => $_->asciiname,
443             email => $_->email,
444             homepage => $_->homepage,
445             };
446             }
447 0         0 \@authors;
448             }
449              
450             sub modules {
451 0     0 1 0 my $self = shift;
452              
453 0         0 my @modules;
454 0         0 for ($self->slurp_packages_details) {
455 0 0       0 /^(\S+)\s+(\S+)\s+(\S+)/ or next;
456 0 0       0 push @modules, {module => $1 ,version => $2 eq 'undef' ? undef : $2, file => $3};
457             }
458 0         0 \@modules;
459             }
460              
461             sub files {
462 0     0 1 0 my $self = shift;
463              
464 0         0 my %files;
465 0         0 for ($self->slurp_packages_details) {
466 0 0       0 /^\S+\s+\S+\s+(\S+)/ or next;
467 0         0 $files{$1} = 1;
468             }
469 0         0 [keys %files];
470             }
471              
472             sub latest_distributions {
473 0     0 1 0 my $self = shift;
474              
475 0         0 require CPAN::DistnameInfo;
476              
477 0         0 my %dists;
478 0 0       0 for my $file (@{ $self->files || [] }) {
  0         0  
479 0         0 my $dist = CPAN::DistnameInfo->new($file);
480 0 0       0 my $name = $dist->dist or next;
481 0 0 0     0 if (
482             !exists $dists{$name}
483             or Parse::PMFile->_vlt($dists{$name}->version, $dist->version)
484             ) {
485 0         0 $dists{$name} = $dist;
486             }
487             }
488 0         0 [values %dists];
489             }
490              
491             sub DESTROY {
492 1     1   1746 my $self = shift;
493 1 50 33     9 if ($self->{cleanup} && $$ == $self->{pid}) {
494 1         3 $self->{root}->remove;
495             }
496             }
497              
498             1;
499              
500             __END__