File Coverage

blib/lib/OrePAN2/Indexer.pm
Criterion Covered Total %
statement 158 178 88.7
branch 24 38 63.1
condition 4 9 44.4
subroutine 32 34 94.1
pod 0 6 0.0
total 218 265 82.2


line stmt bran cond sub pod time code
1             package OrePAN2::Indexer;
2              
3 12     12   969098 use Moo;
  12         22043  
  12         71  
4             with 'OrePAN2::Role::HasLogger';
5              
6 12     12   7540 use feature qw( state );
  12         32  
  12         1586  
7 12     12   69 use utf8;
  12         33  
  12         79  
8              
9 12     12   8275 use Archive::Extract ();
  12         2410257  
  12         576  
10 12     12   6008 use CPAN::Meta 2.131560 ();
  12         383272  
  12         416  
11 12     12   104 use File::Basename ();
  12         22  
  12         314  
12 12     12   69 use File::Find qw( find );
  12         20  
  12         823  
13 12     12   82 use File::Spec ();
  12         18  
  12         239  
14 12     12   1186 use File::Temp qw( tempdir );
  12         16839  
  12         841  
15 12     12   2750 use File::pushd qw( pushd );
  12         7544  
  12         652  
16 12     12   7210 use IO::Zlib ();
  12         657659  
  12         413  
17 12     12   6378 use MetaCPAN::Client ();
  12         4404700  
  12         413  
18 12     12   5963 use OrePAN2::Index ();
  12         48  
  12         416  
19 12     12   6414 use Parse::LocalDistribution ();
  12         387329  
  12         534  
20 12     12   10419 use Path::Tiny ();
  12         177073  
  12         583  
21 12     12   120 use Try::Tiny qw( catch try );
  12         28  
  12         1189  
22 12     12   10339 use Type::Params qw( signature );
  12         80717  
  12         202  
23 12     12   5915 use Types::Standard qw( Bool HashRef Str is_ArrayRef );
  12         29  
  12         109  
24 12     12   55050 use Types::Common::Numeric qw( PositiveInt );
  12         389439  
  12         148  
25 12     12   21953 use Types::Self qw( Self );
  12         162529  
  12         176  
26 12     12   11949 use Types::Path::Tiny qw( Path );
  12         578029  
  12         158  
27              
28 12     12   8303 use namespace::clean;
  12         30  
  12         151  
29              
30             #<<<
31             has directory => ( is => 'ro', isa => Path, coerce => 1, required => 1 );
32             has simple => ( is => 'ro', isa => Bool, default => !!0 );
33             has metacpan => ( is => 'ro', isa => Bool, default => !!0 );
34             has metacpan_lookup_size => ( is => 'ro', isa => PositiveInt, default => 200 );
35             has _metacpan_lookup => ( is => 'rw', isa => HashRef, init_arg => undef );
36             #>>>
37              
38             sub make_index {
39 14     14 0 337409 state $signature = signature(
40             named_to_list => !!1,
41             method => Self,
42             named => [ no_compress => Bool, { default => !!0 } ]
43             );
44 14         733081 my ( $self, $no_compress ) = $signature->(@_);
45              
46 14         651 my @files = $self->list_archive_files();
47              
48 14 100       142 if ( $self->metacpan ) {
49             try {
50 4     4   399 $self->do_metacpan_lookup( \@files );
51             }
52             catch {
53 0     0   0 $self->log->warn("Unable to fetch provides via MetaCPAN");
54 0         0 $self->log->warn("$_");
55 4         66 };
56             }
57              
58 14         9923 my $index = OrePAN2::Index->new();
59 14         612 for my $archive_file (@files) {
60 13         73 $self->add_index( $index, $archive_file );
61             }
62 14         127 $self->write_index( $index, $no_compress );
63 14         5443 return $index;
64             }
65              
66             sub add_index {
67 13     13 0 44 my ( $self, $index, $archive_file ) = @_;
68              
69 13 100       78 return if $self->_maybe_index_from_metacpan( $index, $archive_file );
70              
71 7         95 my $archive = Archive::Extract->new( archive => $archive_file );
72 7         1930 my $tmpdir = tempdir( 'orepan2.XXXXXX', TMPDIR => 1, CLEANUP => 1 );
73 7         4485 $archive->extract( to => $tmpdir );
74              
75 7         1519703 my $provides = $self->scan_provides( $tmpdir, $archive_file );
76 7         598 my $path = $self->_orepan_archive_path($archive_file);
77              
78 7         20 foreach my $package ( sort keys %{$provides} ) {
  7         52  
79             $index->add_index(
80             $package,
81             $provides->{$package}->{version},
82 7         145 $path,
83             );
84             }
85             }
86              
87             sub _orepan_archive_path {
88 13     13   41 my $self = shift;
89 13         52 my $archive_file = shift;
90 13         382 my $path = File::Spec->abs2rel(
91             $archive_file,
92             File::Spec->catfile( $self->directory, 'authors', 'id' )
93             );
94 13         1546 $path =~ s!\\!/!g;
95 13         52 return $path;
96             }
97              
98             sub scan_provides {
99 7     7 0 60 my ( $self, $dir, $archive_file ) = @_;
100              
101 7         1062 my $guard = pushd( glob("$dir/*") );
102 7         1050 for my $mfile ( 'META.json', 'META.yml', 'META.yaml' ) {
103 7 50       141 next unless -f $mfile;
104 7         27 my $meta = eval { CPAN::Meta->load_file($mfile) };
  7         183  
105 7 50 33     114001 return $meta->{provides} if $meta && $meta->{provides};
106              
107 0 0       0 if ($@) {
108 0         0 $self->log->warn("Error using '$mfile' from '$archive_file'");
109 0         0 $self->log->warn("$@");
110 0         0 $self->log->warn("Attempting to continue...");
111             }
112             }
113              
114             $self->log->info(
115 0         0 "Found META file in '$archive_file' but it does not contain 'provides'"
116             );
117 0         0 $self->log->info("Scanning for provided modules...");
118              
119 0         0 my $provides = eval { $self->_scan_provides('.') };
  0         0  
120 0 0       0 return $provides if $provides;
121              
122 0         0 $self->log->warn("Error scanning: $@");
123              
124             # Return empty provides.
125 0         0 return {};
126             }
127              
128             sub _maybe_index_from_metacpan {
129 13     13   40 my ( $self, $index, $file ) = @_;
130              
131 13 100       98 return unless $self->metacpan;
132              
133 6         52 my $archive = Path::Tiny->new($file)->basename;
134 6         810 my $lookup = $self->_metacpan_lookup;
135              
136 6 50       73 unless ( exists $lookup->{archive}->{$archive} ) {
137 0         0 $self->log->info("$archive not found on MetaCPAN");
138 0         0 return;
139             }
140 6         19 my $release_name = $lookup->{archive}->{$archive};
141              
142 6         19 my $provides = $lookup->{release}->{$release_name};
143 6 50 33     27 unless ( $provides && keys %{$provides} ) {
  6         33  
144 0         0 $self->log->info("provides for $archive not found on MetaCPAN");
145 0         0 return;
146             }
147              
148 6         23 my $path = $self->_orepan_archive_path($file);
149              
150 6         11 foreach my $package ( keys %{$provides} ) {
  6         36  
151 45         126 $index->add_index( $package, $provides->{$package}, $path, );
152             }
153 6         40 return 1;
154             }
155              
156             sub do_metacpan_lookup {
157 5     5 0 3454 my ( $self, $files ) = @_;
158              
159 5 50       16 return unless @{$files};
  5         22  
160              
161 5         190 my $provides = $self->_metacpan_lookup;
162              
163 5         241 my $mc = MetaCPAN::Client->new( version => 'v1' );
164 5         2043 my @archives = map { Path::Tiny->new($_)->basename } @{$files};
  8         370  
  5         17  
165 5         476 my @search_by_archives = map { +{ archive => $_ } } @archives;
  8         35  
166              
167 5         25 while (@search_by_archives) {
168 6         278328 my @search_by_archives_chunk = splice @search_by_archives, 0,
169             $self->metacpan_lookup_size;
170              
171 6         44 my $releases
172             = $mc->release( { either => \@search_by_archives_chunk } );
173              
174 6         1606356 my @file_search;
175              
176 6         38 while ( my $release = $releases->next ) {
177 8         5515 $provides->{archive}->{ $release->archive } = $release->name;
178              
179 8         693 push @file_search,
180             {
181             all => [
182             { release => $release->name },
183             { indexed => 'true' },
184             { authorized => 'true' },
185             { 'module.indexed' => 'true' },
186             ]
187             };
188             }
189              
190 6 50       865 next unless @file_search;
191              
192 6         50 my $modules = $mc->module( { either => \@file_search } );
193              
194 6         1123015 while ( my $file = $modules->next ) {
195 65 50       36535 my $module = $file->module or next;
196 65 50       1160 for my $inner ( is_ArrayRef($module) ? @{$module} : $module ) {
  65         167  
197 65 50       286 next unless $inner->{indexed};
198             $provides->{release}->{ $file->release }->{ $inner->{name} }
199 65   66     2054 //= $inner->{version};
200             }
201             }
202             }
203              
204 5         1428207 $self->_metacpan_lookup($provides);
205             }
206              
207             sub _scan_provides {
208 0     0   0 my ( $self, $dir, $meta ) = @_;
209              
210 0         0 my $provides = Parse::LocalDistribution->new( { ALLOW_DEV_VERSION => 1 } )
211             ->parse($dir);
212 0         0 return $provides;
213             }
214              
215             sub write_index {
216 14     14 0 67 my ( $self, $index, $no_compress ) = @_;
217              
218 14 100       312 my $pkgfname = File::Spec->catfile(
219             $self->directory,
220             'modules',
221             $no_compress ? '02packages.details.txt' : '02packages.details.txt.gz'
222             );
223 14         3375 mkdir( File::Basename::dirname($pkgfname) );
224 14         171 my $fh = do {
225 14 100       68 if ($no_compress) {
226 6         987 open my $fh, '>:raw', $pkgfname;
227 6         72 $fh;
228             }
229             else {
230 8 50       168 IO::Zlib->new( $pkgfname, 'w' )
231             or die "Cannot open $pkgfname for writing: $!\n";
232             }
233             };
234 14         21305 print $fh $index->as_string( { simple => $self->{simple} } );
235 14         2203 close $fh;
236             }
237              
238             sub list_archive_files {
239 14     14 0 41 my $self = shift;
240              
241 14         384 my $authors_dir = File::Spec->catfile( $self->directory, 'authors' );
242 14 100       707 return () unless -d $authors_dir;
243              
244 11         29 my @files;
245             find(
246             {
247             wanted => sub {
248 74 100   74   9476 return unless /
249             (?:
250             \.tar\.gz
251             | \.tgz
252             | \.zip
253             )
254             \z/x;
255 13         241 push @files, $_;
256             },
257 11         1463 no_chdir => 1,
258             },
259             $authors_dir
260             );
261              
262             # Sort files by modication time so that we can index distributions from
263             # earliest to latest version.
264              
265 11         114 return sort { -M $b <=> -M $a } @files;
  2         73  
266             }
267              
268             1;