File Coverage

lib/WorePAN.pm
Criterion Covered Total %
statement 150 301 49.8
branch 34 144 23.6
condition 13 55 23.6
subroutine 23 40 57.5
pod 18 18 100.0
total 238 558 42.6


line stmt bran cond sub pod time code
1             package WorePAN;
2              
3 13     13   246544 use strict;
  13         30  
  13         543  
4 13     13   62 use warnings;
  13         18  
  13         383  
5 13     13   10124 use File::Temp ();
  13         250743  
  13         313  
6 13     13   5801 use Path::Extended::Tiny ();
  13         186355  
  13         376  
7 13     13   103 use File::Spec;
  13         25  
  13         250  
8 13     13   8940 use HTTP::Tiny;
  13         427781  
  13         41375  
9              
10             our $VERSION = '0.15';
11              
12             sub new {
13 1     1 1 3596 my ($class, %args) = @_;
14              
15 1 50       8 $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       7 if (!$args{root}) {
27 1 50       8 $args{root} = File::Temp::tempdir(CLEANUP => 1, ($args{tmp} ? (DIR => $args{tmp}) : TMPDIR => 1));
28 1 50       537 warn "'root' is missing; created a temporary WorePAN directory: $args{root}\n" if $args{verbose};
29             }
30 1         10 $args{root} = Path::Extended::Tiny->new($args{root})->mkdir;
31 1   50     59 $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     9 $args{no_network} = 1 if !defined $args{no_network} && $ENV{HARNESS_ACTIVE};
36              
37 1         3 $args{pid} = $$;
38              
39 1   33     55 $args{ua} ||= HTTP::Tiny->new(
40             agent => "WorePAN/$VERSION",
41             );
42              
43 1         107 my $self = bless \%args, $class;
44              
45 1 50       4 my @files = @{ delete $self->{files} || [] };
  1         9  
46 1 50       4 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       5 if (@files) {
54 1         6 $self->_fetch(\@files);
55 1         5745 $self->update_indices(%args);
56             }
57              
58 1         10 $self;
59             }
60              
61 0     0 1 0 sub root { shift->{root} }
62             sub file {
63 1     1 1 42 my $self = shift;
64 1 50       15 my $file = $self->_normalize(File::Spec->catfile(@_)) or return;
65 1         5 $self->{root}->file('authors/id', $file);
66             }
67 1     1 1 6 sub whois { shift->{root}->file('authors/00whois.xml') }
68 1     1 1 6 sub mailrc { shift->{root}->file('authors/01mailrc.txt.gz') }
69 1     1 1 13 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   3 my ($self, $files) = @_;
124              
125 1         1 my %authors;
126             my %packages;
127 1         6 my $_root = $self->{root}->subdir('authors/id');
128 1         99 for my $file (@$files) {
129 1         2 my $dest;
130 1 50 33     31 if (-f $file && $file =~ /\.(?:tar\.(?:gz|bz2)|tgz|zip)$/) {
131 1         4 my $source = Path::Extended::Tiny->new($file);
132 1         26 $dest = $_root->file('L/LO/LOCAL/', $source->basename);
133 1         83 $self->_log("copy $source to $dest");
134 1         4 $source->copy_to($dest);
135 1         8067 $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   2 my ($self, $file) = @_;
146              
147 1 50       4 $file =~ s|\\|/|g if $^O eq 'MSWin32';
148              
149 1 50       9 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         5 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 0 0       0 $uri->query_param(d => [
175 0         0 map { $dists->{$_} ? "$_,$dists->{$_}" : $_ } @tmp
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 0     0 $_->{filename} && $_->{author}
  0         0  
193             ? join '/',
194             substr($_->{author}, 0, 1),
195             substr($_->{author}, 0, 2),
196             $_->{author},
197             $_->{filename}
198             : ()
199             } @files;
200             }
201              
202             sub _log {
203 4     4   37 my ($self, $message) = @_;
204 4 50       22 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     0   0 my %args = (@_ == 1 && ref $_[0] eq ref sub {})
243 1 50 33     12 ? (callback => $_[0])
244             : @_;
245 1         6 my $root = $self->{root}->subdir('authors/id');
246 1   33     121 my $tmproot = $self->{tmp} || $args{tmp};
247 1   33     6 my $allow_dev_releases = $args{developer_releases} || $self->{developer_releases};
248              
249 1         475 require Archive::Any::Lite;
250              
251 1         33166 local $Archive::Any::Lite::IGNORE_SYMLINK = 1;
252             $root->recurse(callback => sub {
253 5     5   914 my $archive_file = shift;
254 5 100       18 return if -d $archive_file;
255              
256 1         18 my $path = $archive_file->relative($root);
257 1         143 my $basename = $archive_file->basename;
258 1 50       48 return unless $basename =~ /\.(?:tar\.(?:gz|bz2)|tgz|zip)$/;
259 1 50       3 return if $basename =~ /^perl\-\d+/; # perls
260 1 50 33     14 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       62 my $tmpdir = Path::Extended::Tiny->new(File::Temp::tempdir(CLEANUP => 1, ($tmproot ? (DIR => $tmproot) : (TMPDIR => 1))));
267 1         449 $archive->extract($tmpdir);
268 1 50       4151 my $basedir = $tmpdir->children == 1 ? ($tmpdir->children)[0] : $tmpdir;
269 1 50       326 $basedir = $tmpdir unless -d $basedir;
270              
271 1         22 $args{callback}->($basedir, $path, $archive_file);
272              
273 1         10 $tmpdir->remove;
274 1         15 });
275             }
276              
277             sub update_indices {
278 1     1 1 6 my ($self, %args) = @_;
279              
280 1 50       5 return if $self->{no_indices};
281              
282 1         8 require IO::Zlib;
283 1         530 require Parse::PMFile;
284 1         69075 require Parse::LocalDistribution;
285              
286 1   33     3539 my $allow_dev_releases = $args{developer_releases} || $self->{developer_releases};
287 1   33     28 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         5 my $mtime = $archive_file->mtime;
294 1         199 my ($author) = $path =~ m{^[A-Z]/[A-Z][A-Z0-9_]/([^/]+)/};
295 1         10 $authors{$author} = 1;
296              
297             # a dist that has blib/ shouldn't be indexed
298             # see PAUSE::dist::mail_summary
299 1 50 33     5 return if $basedir->basename eq 'blib' or $basedir->subdir('blib')->exists;
300              
301 1         162 my $args = {ALLOW_DEV_VERSION => $allow_dev_releases};
302 1 50       3 if ($permissions) {
303 0         0 $args->{PERMISSIONS} = $permissions;
304 0         0 $args->{USERID} = $author;
305             }
306 1         9 my $parser = Parse::LocalDistribution->new($args);
307 1         3541 my $info = $parser->parse($basedir);
308 1         6161 $self->_update_packages(\%packages, $info, $path, $mtime);
309 1         13 });
310 1         979 $self->_write_whois(\%authors);
311 1         5 $self->_write_mailrc(\%authors);
312 1         4 $self->_write_packages_details(\%packages);
313              
314 1         10 return 1;
315             }
316              
317             sub _update_packages {
318 1     1   5 my ($self, $packages, $info, $path, $mtime) = @_;
319              
320 1         5 for my $module (sort keys %$info) {
321 1 50       5 next unless exists $info->{$module}{version};
322 1         2 my $new_version = $info->{$module}{version};
323 1 50       3 if (!$packages->{$module}) { # shortcut
324 1         4 $packages->{$module} = [$new_version, $path, $mtime];
325 1         15 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 0 0 0     0 if (
      0        
337             $new_version eq 'undef' or $new_version == 0 or
338             Parse::PMFile->_vcmp($new_version, $cur_version) == 0
339             ) {
340 0 0       0 if ($mtime >= $packages->{$module}[2]) {
341 0         0 $ok++; # dist is newer
342             }
343             }
344             }
345 0 0       0 if ($ok) {
346 0         0 $packages->{$module} = [$new_version, $path, $mtime];
347             }
348             }
349             }
350              
351             sub _write_whois {
352 1     1   3 my ($self, $authors) = @_;
353              
354 1         6 my $index = $self->whois;
355 1         157 $index->parent->mkdir;
356 1         99 $index->openw;
357 1         141 $index->printf(qq{\n\n}, scalar(gmtime), $VERSION);
358 1         28 for my $id (sort keys %$authors) {
359 1         6 $index->printf("%sauthor%s%s\@cpan.org\n", $id, $id, lc $id);
360             }
361 1         12 $index->print("\n");
362 1         15 $index->close;
363 1         54 $self->_log("created $index");
364             }
365              
366             sub _write_mailrc {
367 1     1   3 my ($self, $authors) = @_;
368              
369 1         5 my $index = $self->mailrc;
370 1         70 $index->parent->mkdir;
371 1 50       76 my $fh = IO::Zlib->new($index->path, "wb") or die $!;
372 1         1786 for my $id (sort keys %$authors) {
373 1         22 $fh->printf("alias %s \"%s <%s\@cpan.org>\"\n", $id, $id, lc $id);
374             }
375 1         180 $fh->close;
376 1         340 $self->_log("created $index");
377             }
378              
379             sub _write_packages_details {
380 1     1   2 my ($self, $packages) = @_;
381              
382 1         5 my $index = $self->packages_details;
383 1         82 $index->parent->mkdir;
384 1 50       234 my $fh = IO::Zlib->new($index->path, "wb") or die $!;
385 1         1099 $fh->print("File: 02packages.details.txt\n");
386 1         133 $fh->print("Last-Updated: ".localtime(time)."\n");
387 1         82 $fh->print("\n");
388 1 0       71 for my $pkg (map {$_->[1]} sort {($a->[0] cmp $b->[0]) || ($a->[1] cmp $b->[1])} map {[lc $_, $_]} keys %$packages) {
  1         5  
  0         0  
  1         7  
389 1         4 my ($first, $second) = (30, 8);
390 1 50       6 my $ver = defined $packages->{$pkg}[0] ? $packages->{$pkg}[0] : 'undef';
391 1 50       5 if (length($pkg) > $first) {
392 0         0 $second = length($ver);
393 0         0 $first += 8 - $second;
394             }
395 1         7 $fh->printf("%-${first}s %${second}s %s\n",
396             $pkg,
397             $ver,
398             $packages->{$pkg}[1]
399             );
400             }
401 1         87 $fh->close;
402 1         241 $self->_log("created $index");
403             }
404              
405             sub look_for {
406 0     0 1 0 my ($self, $package) = @_;
407              
408 0 0       0 return unless defined $package;
409              
410 0         0 for ($self->slurp_packages_details) {
411 0 0       0 if (/^$package\s+(\S+)\s+(\S+)$/) {
412 0 0       0 return wantarray ? ($1, $2) : $1;
413             }
414             }
415 0         0 return;
416             }
417              
418 0     0 1 0 sub authors { shift->_authors_whois }
419              
420             sub _authors_mailrc {
421 0     0   0 my $self = shift;
422              
423 0         0 my @authors;
424 0         0 for ($self->slurp_mailrc) {
425 0         0 my ($id, $name, $email) = /^alias\s+(\S+)\s+"?(.+?)\s+(\S+?)"?\s*$/;
426 0 0       0 next unless $id;
427 0         0 $email =~ tr/<>//d;
428 0         0 push @authors, {pauseid => $id, name => $name, email => $email};
429             }
430 0         0 \@authors;
431             }
432              
433             sub _authors_whois {
434 0     0   0 my $self = shift;
435              
436 0         0 my @authors;
437 0         0 for ($self->slurp_whois) {
438 0         0 push @authors, {
439             pauseid => $_->pauseid,
440             name => $_->name,
441             asciiname => $_->asciiname,
442             email => $_->email,
443             homepage => $_->homepage,
444             };
445             }
446 0         0 \@authors;
447             }
448              
449             sub modules {
450 0     0 1 0 my $self = shift;
451              
452 0         0 my @modules;
453 0         0 for ($self->slurp_packages_details) {
454 0 0       0 /^(\S+)\s+(\S+)\s+(\S+)/ or next;
455 0 0       0 push @modules, {module => $1 ,version => $2 eq 'undef' ? undef : $2, file => $3};
456             }
457 0         0 \@modules;
458             }
459              
460             sub files {
461 0     0 1 0 my $self = shift;
462              
463 0         0 my %files;
464 0         0 for ($self->slurp_packages_details) {
465 0 0       0 /^\S+\s+\S+\s+(\S+)/ or next;
466 0         0 $files{$1} = 1;
467             }
468 0         0 [keys %files];
469             }
470              
471             sub latest_distributions {
472 0     0 1 0 my $self = shift;
473              
474 0         0 require CPAN::DistnameInfo;
475              
476 0         0 my %dists;
477 0 0       0 for my $file (@{ $self->files || [] }) {
  0         0  
478 0         0 my $dist = CPAN::DistnameInfo->new($file);
479 0 0       0 my $name = $dist->dist or next;
480 0 0 0     0 if (
481             !exists $dists{$name}
482             or Parse::PMFile->_vlt($dists{$name}->version, $dist->version)
483             ) {
484 0         0 $dists{$name} = $dist;
485             }
486             }
487 0         0 [values %dists];
488             }
489              
490             sub DESTROY {
491 1     1   2546 my $self = shift;
492 1 50 33     12 if ($self->{cleanup} && $$ == $self->{pid}) {
493 1         4 $self->{root}->remove;
494             }
495             }
496              
497             1;
498              
499             __END__