File Coverage

blib/lib/OrePAN2/Indexer.pm
Criterion Covered Total %
statement 151 173 87.2
branch 28 44 63.6
condition 4 9 44.4
subroutine 31 33 93.9
pod 0 9 0.0
total 214 268 79.8


line stmt bran cond sub pod time code
1             package OrePAN2::Indexer;
2              
3 6     6   705469 use strict;
  6         41  
  6         191  
4 6     6   30 use warnings;
  6         10  
  6         208  
5 6     6   32 use utf8;
  6         28  
  6         44  
6              
7 6     6   3998 use Archive::Extract ();
  6         1080106  
  6         288  
8 6     6   3767 use CPAN::Meta 2.131560;
  6         178623  
  6         235  
9 6     6   2428 use Class::Accessor::Lite ( rw => ['_metacpan_lookup'] );
  6         4818  
  6         60  
10 6     6   566 use File::Basename ();
  6         15  
  6         125  
11 6     6   41 use File::Find qw(find);
  6         12  
  6         341  
12 6     6   42 use File::Spec ();
  6         12  
  6         122  
13 6     6   38 use File::Temp qw(tempdir);
  6         13  
  6         284  
14 6     6   1563 use File::pushd;
  6         3873  
  6         326  
15 6     6   3549 use IO::Zlib;
  6         363948  
  6         53  
16 6     6   3380 use MetaCPAN::Client;
  6         1704205  
  6         249  
17 6     6   3285 use OrePAN2::Index;
  6         28  
  6         274  
18 6     6   3520 use Parse::LocalDistribution;
  6         321225  
  6         262  
19 6     6   5524 use Path::Tiny;
  6         65868  
  6         396  
20 6     6   3603 use Try::Tiny;
  6         8282  
  6         432  
21 6     6   54 use Ref::Util qw(is_arrayref);
  6         12  
  6         10505  
22              
23             sub new {
24 13     13 0 17259 my $class = shift;
25 13 50       115 my %args = @_ == 1 ? %{ $_[0] } : @_;
  0         0  
26 13 50       74 unless ( defined $args{directory} ) {
27 0         0 Carp::croak('Missing mandatory parameter: directory');
28             }
29             bless {
30 13         179 %args,
31             }, $class;
32             }
33              
34 23     23 0 1577 sub directory { shift->{directory} }
35              
36 6 100   6 0 50 sub metacpan_lookup_size { shift->{metacpan_lookup_size} || 200 }
37              
38             sub make_index {
39 12     12 0 105 my ( $self, %args ) = @_;
40              
41 12         58 my @files = $self->list_archive_files();
42              
43 12 100       63 if ( $self->{metacpan} ) {
44             try {
45 4     4   293 $self->do_metacpan_lookup( \@files );
46             }
47             catch {
48 0     0   0 print STDERR "[WARN] Unable to fetch provides via MetaCPAN\n";
49 0         0 print STDERR "[WARN] $_\n";
50 4         59 };
51             }
52              
53 12         2064 my $index = OrePAN2::Index->new();
54 12         40 for my $archive_file (@files) {
55 11         52 $self->add_index( $index, $archive_file );
56             }
57 12         157 $self->write_index( $index, $args{no_compress} );
58 12         3943 return $index;
59             }
60              
61             sub add_index {
62 11     11 0 36 my ( $self, $index, $archive_file ) = @_;
63              
64 11 100       66 return if $self->_maybe_index_from_metacpan( $index, $archive_file );
65              
66 5         59 my $archive = Archive::Extract->new( archive => $archive_file );
67 5         1592 my $tmpdir = tempdir( 'orepan2.XXXXXX', TMPDIR => 1, CLEANUP => 1 );
68 5         2520 $archive->extract( to => $tmpdir );
69              
70 5         891155 my $provides = $self->scan_provides( $tmpdir, $archive_file );
71 5         433 my $path = $self->_orepan_archive_path($archive_file);
72              
73 5         13 foreach my $package ( sort keys %{$provides} ) {
  5         49  
74             $index->add_index(
75             $package,
76             $provides->{$package}->{version},
77 5         117 $path,
78             );
79             }
80             }
81              
82             sub _orepan_archive_path {
83 11     11   28 my $self = shift;
84 11         34 my $archive_file = shift;
85 11         209 my $path = File::Spec->abs2rel(
86             $archive_file,
87             File::Spec->catfile( $self->directory, 'authors', 'id' )
88             );
89 11         103 $path =~ s!\\!/!g;
90 11         45 return $path;
91             }
92              
93             sub scan_provides {
94 5     5 0 62 my ( $self, $dir, $archive_file ) = @_;
95              
96 5         935 my $guard = pushd( glob("$dir/*") );
97 5         1051 for my $mfile ( 'META.json', 'META.yml', 'META.yaml' ) {
98 5 50       140 next unless -f $mfile;
99 5         26 my $meta = eval { CPAN::Meta->load_file($mfile) };
  5         231  
100 5 50 33     93119 return $meta->{provides} if $meta && $meta->{provides};
101              
102 0 0       0 if ($@) {
103 0         0 print STDERR "[WARN] Error using '$mfile' from '$archive_file'\n";
104 0         0 print STDERR "[WARN] $@\n";
105 0         0 print STDERR "[WARN] Attempting to continue...\n";
106             }
107             }
108              
109 0         0 print STDERR
110             "[INFO] Found META file in '$archive_file' but it does not contain 'provides'\n";
111 0         0 print STDERR "[INFO] Scanning for provided modules...\n";
112              
113 0         0 my $provides = eval { $self->_scan_provides('.') };
  0         0  
114 0 0       0 return $provides if $provides;
115              
116 0         0 print STDERR "[WARN] Error scanning: $@\n";
117              
118             # Return empty provides.
119 0         0 return {};
120             }
121              
122             sub _maybe_index_from_metacpan {
123 11     11   39 my ( $self, $index, $file ) = @_;
124              
125 11 100       51 return unless $self->{metacpan};
126              
127 6         58 my $archive = Path::Tiny->new($file)->basename;
128 6         529 my $lookup = $self->_metacpan_lookup;
129              
130 6 50       49 unless ( exists $lookup->{archive}->{$archive} ) {
131 0         0 print STDERR "[INFO] $archive not found on MetaCPAN\n";
132 0         0 return;
133             }
134 6         15 my $release_name = $lookup->{archive}->{$archive};
135              
136 6         17 my $provides = $lookup->{release}->{$release_name};
137 6 50 33     23 unless ( $provides && keys %{$provides} ) {
  6         38  
138 0         0 print STDERR "[INFO] provides for $archive not found on MetaCPAN\n";
139 0         0 return;
140             }
141              
142 6         25 my $path = $self->_orepan_archive_path($file);
143              
144 6         11 foreach my $package ( keys %{$provides} ) {
  6         29  
145 43         97 $index->add_index( $package, $provides->{$package}, $path, );
146             }
147 6         31 return 1;
148             }
149              
150             sub do_metacpan_lookup {
151 5     5 0 50 my ( $self, $files ) = @_;
152              
153 5 50       14 return unless @{$files};
  5         23  
154              
155 5         39 my $provides = $self->_metacpan_lookup;
156              
157 5         231 my $mc = MetaCPAN::Client->new( version => 'v1' );
158 5         1666 my @archives = map { Path::Tiny->new($_)->basename } @{$files};
  8         347  
  5         15  
159 5         318 my @search_by_archives = map { +{ archive => $_ } } @archives;
  8         30  
160              
161 5         21 while (@search_by_archives) {
162             my @search_by_archives_chunk
163 6         215389 = splice @search_by_archives, 0, $self->metacpan_lookup_size;
164              
165 6         42 my $releases = $mc->release( { either => \@search_by_archives_chunk } );
166              
167 6         1198128 my @file_search;
168              
169 6         35 while ( my $release = $releases->next ) {
170 8         388120 $provides->{archive}->{ $release->archive } = $release->name;
171              
172 8         566 push @file_search,
173             {
174             all => [
175             { release => $release->name },
176             { indexed => 'true' },
177             { authorized => 'true' },
178             { 'module.indexed' => 'true' },
179             ]
180             };
181             }
182              
183 6 50       255 next unless @file_search;
184              
185 6         54 my $modules = $mc->module( { either => \@file_search } );
186              
187 6         1245061 while ( my $file = $modules->next ) {
188 61 50       405842 my $module = $file->module or next;
189 61 50       915 for my $inner ( is_arrayref $module ? @{$module} : $module ) {
  61         130  
190 61 50       223 next unless $inner->{indexed};
191             $provides->{release}->{ $file->release }->{ $inner->{name} } //=
192 61   66     1397 $inner->{version};
193             }
194             }
195             }
196              
197 5         821488 $self->_metacpan_lookup($provides);
198             }
199              
200             sub _scan_provides {
201 0     0   0 my ( $self, $dir, $meta ) = @_;
202              
203 0         0 my $provides = Parse::LocalDistribution->new( { ALLOW_DEV_VERSION => 1 } )
204             ->parse($dir);
205 0         0 return $provides;
206             }
207              
208             sub write_index {
209 12     12 0 67 my ( $self, $index, $no_compress ) = @_;
210              
211 12 100       59 my $pkgfname = File::Spec->catfile(
212             $self->directory,
213             'modules',
214             $no_compress ? '02packages.details.txt' : '02packages.details.txt.gz'
215             );
216 12         1969 mkdir( File::Basename::dirname($pkgfname) );
217 12         66 my $fh = do {
218 12 100       60 if ($no_compress) {
219 5         552 open my $fh, '>:raw', $pkgfname;
220 5         34 $fh;
221             }
222             else {
223 7 50       152 IO::Zlib->new( $pkgfname, 'w' )
224             or die "Cannot open $pkgfname for writing: $!\n";
225             }
226             };
227 12         18950 print $fh $index->as_string( { simple => $self->{simple} } );
228 12         2386 close $fh;
229             }
230              
231             sub list_archive_files {
232 12     12 0 56 my $self = shift;
233              
234 12         157 my $authors_dir = File::Spec->catfile( $self->{directory}, 'authors' );
235 12 100       293 return () unless -d $authors_dir;
236              
237 9         31 my @files;
238             find(
239             {
240             wanted => sub {
241 62 100   62   4698 return unless /
242             (?:
243             \.tar\.gz
244             | \.tgz
245             | \.zip
246             )
247             \z/x;
248 11         166 push @files, $_;
249             },
250 9         1253 no_chdir => 1,
251             },
252             $authors_dir
253             );
254              
255             # Sort files by modication time so that we can index distributions from
256             # earliest to latest version.
257              
258 9         96 return sort { -M $b <=> -M $a } @files;
  2         76  
259             }
260              
261             1;