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   1821933 use strict;
  13         30  
  13         543  
4 13     13   70 use warnings;
  13         32  
  13         878  
5 13     13   12964 use File::Temp ();
  13         395694  
  13         2576  
6 13     13   22273 use Path::Extended::Tiny ();
  13         299951  
  13         513  
7 13     13   132 use File::Spec;
  13         25  
  13         314  
8 13     13   11415 use HTTP::Tiny;
  13         765417  
  13         47883  
9              
10             our $VERSION = '0.18';
11              
12             sub new {
13 1     1 1 433398 my ($class, %args) = @_;
14              
15 1 50       11 $args{verbose} = $ENV{TEST_VERBOSE} unless defined $args{verbose};
16              
17 1 50       6 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       11 $args{root} = File::Temp::tempdir(CLEANUP => 1, ($args{tmp} ? (DIR => $args{tmp}) : TMPDIR => 1));
28 1 50       946 warn "'root' is missing; created a temporary WorePAN directory: $args{root}\n" if $args{verbose};
29             }
30 1         11 $args{root} = Path::Extended::Tiny->new($args{root})->mkdir;
31 1   50     101 $args{cpan} ||= "https://www.cpan.org/";
32 1 50       5 if ($args{use_backpan}) {
33 0   0     0 $args{backpan} ||= "https://backpan.cpanauthors.org/";
34             }
35 1 50 33     9 $args{no_network} = 1 if !defined $args{no_network} && $ENV{HARNESS_ACTIVE};
36              
37 1         9 $args{pid} = $$;
38              
39 1   33     15 $args{ua} ||= HTTP::Tiny->new(
40             agent => "WorePAN/$VERSION",
41             );
42              
43 1         120 my $self = bless \%args, $class;
44              
45 1 50       3 my @files = @{ delete $self->{files} || [] };
  1         10  
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         26 $self->_fetch(\@files);
55 1         9795 $self->update_indices(%args);
56             }
57              
58 1         66 $self;
59             }
60              
61 0     0 1 0 sub root { shift->{root} }
62             sub file {
63 1     1 1 75 my $self = shift;
64 1 50       23 my $file = $self->_normalize(File::Spec->catfile(@_)) or return;
65 1         8 $self->{root}->file('authors/id', $file);
66             }
67 1     1 1 7 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 6 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         11 my %authors;
126             my %packages;
127 1         6 my $_root = $self->{root}->subdir('authors/id');
128 1         153 for my $file (@$files) {
129 1         3 my $dest;
130 1 50 33     40 if (-f $file && $file =~ /\.(?:tar\.(?:gz|bz2)|tgz|zip)$/) {
131 1         5 my $source = Path::Extended::Tiny->new($file);
132 1         66 $dest = $_root->file('L/LO/LOCAL/', $source->basename);
133 1         176 $self->_log("copy $source to $dest");
134 1         9 $source->copy_to($dest);
135 1         15014 $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   4 my ($self, $file) = @_;
146              
147 1 50       7 $file =~ s|\\|/|g if $^O eq 'MSWin32';
148              
149 1 50       11 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         7 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('https://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   58 my ($self, $message) = @_;
204 4 50       45 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 3 my $self = shift;
242       0     my %args = (@_ == 1 && ref $_[0] eq ref sub {})
243 1 50 33     42 ? (callback => $_[0])
244             : @_;
245 1         14 my $root = $self->{root}->subdir('authors/id');
246 1   33     216 my $tmproot = $self->{tmp} || $args{tmp};
247 1   33     7 my $allow_dev_releases = $args{developer_releases} || $self->{developer_releases};
248              
249 1         713 require Archive::Any::Lite;
250              
251 1         111705 local $Archive::Any::Lite::IGNORE_SYMLINK = 1;
252             $root->recurse(callback => sub {
253 5     5   4623 my $archive_file = shift;
254 5 100       29 return if -d $archive_file;
255              
256 1         26 my $path = $archive_file->relative($root);
257 1         424 my $basename = $archive_file->basename;
258 1 50       30 return unless $basename =~ /\.(?:tar\.(?:gz|bz2)|tgz|zip)$/;
259 1 50       5 return if $basename =~ /^perl\-\d+/; # perls
260 1 50 33     13 return if !$allow_dev_releases && (
      33        
261             $basename =~ /\d\.\d+_\d/ # dev release
262             or $basename =~ /TRIAL/ # trial release
263             );
264              
265 1         7 my $archive = Archive::Any::Lite->new($archive_file->path);
266 1 50       104 my $tmpdir = Path::Extended::Tiny->new(File::Temp::tempdir(CLEANUP => 1, ($tmproot ? (DIR => $tmproot) : (TMPDIR => 1))));
267 1         764 $archive->extract($tmpdir);
268 1 50       7061 my $basedir = $tmpdir->children == 1 ? ($tmpdir->children)[0] : $tmpdir;
269 1 50       685 $basedir = $tmpdir unless -d $basedir;
270              
271 1         34 $args{callback}->($basedir, $path, $archive_file);
272              
273 1         9 $tmpdir->remove;
274 1         21 });
275             }
276              
277             sub update_indices {
278 1     1 1 9 my ($self, %args) = @_;
279              
280 1 50       5 return if $self->{no_indices};
281              
282 1         9 require IO::Zlib;
283 1         699 require Parse::PMFile;
284 1         34539 require Parse::LocalDistribution;
285              
286 1   33     4969 my $allow_dev_releases = $args{developer_releases} || $self->{developer_releases};
287 1   33     10 my $permissions = $args{permissions} || $self->{permissions};
288              
289 1         3 my (%authors, %packages);
290             $self->walk(%args, callback => sub {
291 1     1   4 my ($basedir, $path, $archive_file) = @_;
292              
293 1         35 my $mtime = $archive_file->mtime;
294 1         310 my ($author) = $path =~ m{^[A-Z]/[A-Z][A-Z0-9_]/([^/]+)/};
295 1         13 $authors{$author} = 1;
296              
297             # a dist that has blib/ shouldn't be indexed
298             # see PAUSE::dist::mail_summary
299 1 50 33     6 return if $basedir->basename eq 'blib' or $basedir->subdir('blib')->exists;
300              
301 1         4309 my $args = {ALLOW_DEV_VERSION => $allow_dev_releases};
302 1 50       15 if ($permissions) {
303 0         0 $args->{PERMISSIONS} = $permissions;
304 0         0 $args->{USERID} = $author;
305             }
306 1         14 my $parser = Parse::LocalDistribution->new($args);
307 1         25 my $info = $parser->parse($basedir);
308 1         23945 $self->_update_packages(\%packages, $info, $path, $mtime);
309 1         17 });
310 1         5353 $self->_write_whois(\%authors);
311 1         14 $self->_write_mailrc(\%authors);
312 1         16 $self->_write_packages_details(\%packages);
313              
314 1         17 return 1;
315             }
316              
317             sub _update_packages {
318 1     1   4 my ($self, $packages, $info, $path, $mtime) = @_;
319              
320 1         4 for my $module (sort keys %$info) {
321 1 50       5 next unless exists $info->{$module}{version};
322 1         3 my $new_version = $info->{$module}{version};
323 1 50       4 if (!$packages->{$module}) { # shortcut
324 1         5 $packages->{$module} = [$new_version, $path, $mtime];
325 1         13 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   166 no warnings; # numeric/version
  13         63  
  13         26439  
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   25 my ($self, $authors) = @_;
354              
355 1         6 my $index = $self->whois;
356 1         228 $index->parent->mkdir;
357 1         146 $index->openw;
358 1         327 $index->printf(qq{\n\n}, scalar(gmtime), $VERSION);
359 1         38 for my $id (sort keys %$authors) {
360 1         7 $index->printf("%sauthor%s%s\@cpan.org1\n", $id, $id, lc $id);
361             }
362 1         19 $index->print("\n");
363 1         38 $index->close;
364 1         83 $self->_log("created $index");
365             }
366              
367             sub _write_mailrc {
368 1     1   3 my ($self, $authors) = @_;
369              
370 1         5 my $index = $self->mailrc;
371 1         159 $index->parent->mkdir;
372 1 50       129 my $fh = IO::Zlib->new($index->path, "wb") or die $!;
373 1         2605 for my $id (sort keys %$authors) {
374 1         19 $fh->printf("alias %s \"%s <%s\@cpan.org>\"\n", $id, $id, lc $id);
375             }
376 1         223 $fh->close;
377 1         530 $self->_log("created $index");
378             }
379              
380             sub _write_packages_details {
381 1     1   4 my ($self, $packages) = @_;
382              
383 1         6 my $index = $self->packages_details;
384 1         203 $index->parent->mkdir;
385 1 50       395 my $fh = IO::Zlib->new($index->path, "wb") or die $!;
386 1         3597 $fh->print("File: 02packages.details.txt\n");
387 1         216 $fh->print("Last-Updated: ".localtime(time)."\n");
388 1         162 $fh->print("\n");
389 1 0       181 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         8  
390 1         3 my ($first, $second) = (30, 8);
391 1 50       10 my $ver = defined $packages->{$pkg}[0] ? $packages->{$pkg}[0] : 'undef';
392 1 50       6 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         10 $packages->{$pkg}[1]
400             );
401             }
402 1         170 $fh->close;
403 1         463 $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 Parse::Distname;
476              
477 0         0 my %dists;
478 0 0       0 for my $file (@{ $self->files || [] }) {
  0         0  
479 0         0 my $dist = Parse::Distname->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   4682 my $self = shift;
493 1 50 33     11 if ($self->{cleanup} && $$ == $self->{pid}) {
494 1         3 $self->{root}->remove;
495             }
496             }
497              
498             1;
499              
500             __END__