File Coverage

blib/lib/CPAN/Mini/Indexed.pm
Criterion Covered Total %
statement 38 40 95.0
branch n/a
condition n/a
subroutine 14 14 100.0
pod n/a
total 52 54 96.3


line stmt bran cond sub pod time code
1              
2             package CPAN::Mini::Indexed ;
3              
4 1     1   36940 use strict ;
  1         2  
  1         35  
5 1     1   5 use warnings ;
  1         1  
  1         25  
6 1     1   4 use Carp ;
  1         8  
  1         121  
7              
8             BEGIN
9             {
10 1         10 use Sub::Exporter -setup =>
11             {
12             exports => [ qw(search check_index show_database_information) ],
13             groups =>
14             {
15             all => [ qw() ],
16             }
17 1     1   1020 };
  1         16865  
18            
19 1     1   494 use vars qw ($VERSION);
  1         2  
  1         49  
20 1     1   19 $VERSION = '0.03_01';
21             }
22              
23             #-------------------------------------------------------------------------------
24              
25 1     1   1263 use Time::HiRes 'time' ;
  1         2353  
  1         7  
26 1     1   2387 use File::Temp ;
  1         46749  
  1         108  
27 1     1   1138 use Text::Pluralize;
  1         1337  
  1         60  
28 1     1   1357 use File::Find::Rule ;
  1         15595  
  1         11  
29 1     1   1254 use IO::Zlib ;
  1         111272  
  1         9  
30 1     1   1571 use Archive::Tar ;
  1         796306  
  1         81  
31 1     1   1112 use File::Copy ;
  1         2530  
  1         54  
32              
33 1     1   415 use Search::Indexer::Incremental::MD5 qw() ;
  0            
  0            
34             use Search::Indexer::Incremental::MD5::Indexer qw() ;
35             use Search::Indexer::Incremental::MD5::Searcher qw() ;
36             use Search::Indexer::Incremental::MD5::Language::Perl qw() ;
37              
38             use English qw( -no_match_vars ) ;
39              
40             use Readonly ;
41             Readonly my $EMPTY_STRING => q{} ;
42              
43             #-------------------------------------------------------------------------------
44              
45             =head1 NAME
46              
47             CPAN::Mini::Indexed - Index the content of your CPAN mini repository
48              
49             =head1 SYNOPSIS
50              
51              
52             =head1 DESCRIPTION
53              
54             This module implements ...
55              
56             =head1 DOCUMENTATION
57              
58             =head1 SUBROUTINES/METHODS
59              
60             =cut
61              
62             #----------------------------------------------------------------------------------------------------------
63              
64             sub show_database_information
65             {
66              
67             =head2 ( )
68              
69             some code
70              
71             I
72              
73             =over 2
74              
75             =item * $ -
76              
77             =back
78              
79             I - Nothing
80              
81             I - None
82              
83             =cut
84              
85             my ($options) = @_ ;
86             my $information = Search::Indexer::Incremental::MD5::show_database_information($options->{index_directory}) ;
87              
88             # make sizes more readable
89             1 while $information->{entries} =~ s/^([-+]?\d+)(\d{3})/$1_$2/ ;
90             1 while $information->{size} =~ s/^([-+]?\d+)(\d{3})/$1_$2/ ;
91              
92             print {*STDOUT} <<"EOI" ;
93             Location: $options->{index_directory}
94             Last updated on: $information->{update_date}
95             Number of indexed documents: $information->{entries}
96             Database size: $information->{size} bytes
97             EOI
98             }
99              
100             #----------------------------------------------------------------------------------------------------------
101              
102             sub search
103             {
104              
105             =head2 ( )
106              
107             some code
108              
109             I
110              
111             =over 2
112              
113             =item * $ -
114              
115             =back
116              
117             I - Nothing
118              
119             I - None
120              
121             =cut
122              
123             my ($options) = @_ ;
124              
125             my $searcher
126             = eval
127             {
128             Search::Indexer::Incremental::MD5::Searcher->new
129             (
130             INDEX_DIRECTORY => $options->{index_directory},
131             USE_POSITIONS => 0,
132             WORD_REGEX => qr/\w+/,
133             );
134             } or croak "No full text index found! $EVAL_ERROR\n" ;
135              
136             my $results = $searcher->search(SEARCH_STRING => $options->{search}) ;
137              
138             my @indexes = map { $_->[0] }
139             reverse
140             sort { $a->[1] <=> $b->[1] }
141             map { [$_, $results->[$_]{SCORE}] }
142             0 .. $#$results ;
143              
144             my ($displayed_matches, %displayed_module) = (0) ;
145              
146             for my $index (@indexes)
147             {
148             last if $displayed_matches++ == $options->{lines} ;
149            
150             my $matching_file = $results->[$index]{PATH} ;
151            
152             unless($matching_file)
153             {
154             carp "matched id:'$results->[$index]{ID}' which was removed!\n" ;
155             next ;
156             }
157            
158             (my $matching_file_short = $matching_file) =~ s{^/tmp/[^/]+/}{} ;
159            
160             if($options->{modules_only})
161             {
162             (my $matching_module = $matching_file_short) =~ s{^([^/]+).*}{$1} ;
163             $matching_module =~ s/(.*)-.*/$1/g ;
164             $matching_module =~ s/-/::/g ;
165            
166             print {*STDOUT} "$matching_module\n" unless exists $displayed_module{$matching_module} ;
167            
168             $displayed_module{$matching_module} += $results->[$index]{SCORE} ;
169             }
170             else
171             {
172             if($options->{verbose})
173             {
174             print {*STDOUT} "'$matching_file_short' [id:'$results->[$index]{ID}', score: '$results->[$index]{SCORE}]'\n" ;
175             }
176             else
177             {
178             print {*STDOUT} "$matching_file_short\n" ;
179             }
180             }
181             }
182             }
183              
184             #----------------------------------------------------------------------------------------------------------
185              
186             sub check_index
187             {
188              
189             =head2 check_index($indexer, $options)
190              
191             brings the cpan mini index database up to date
192              
193             I -
194              
195             $indexer, $options
196              
197             I - Nothing
198              
199             I -
200              
201             =cut
202              
203             my ($options) = @_ ;
204              
205             my $cpan_mini = $options->{cpan_mini} || $ENV{CPAN_MINI} || '/devel/cpan' ;
206             $cpan_mini =~ s{^\./}[] ;
207             $cpan_mini =~ s{/$}[] ;
208              
209             croak "Invalid cpan mini repository!\n" if $cpan_mini eq $EMPTY_STRING;
210              
211             printf "[CPAN mini repository in '$cpan_mini']\n" if ($options->{verbose}) ;
212              
213             my $modules_details_file = "$cpan_mini/modules/02packages.details.txt.gz" ;
214             my $cache_details_file = "$options->{index_directory}/02packages.details.txt.gz" ;
215              
216             if(index_needs_update($modules_details_file, $cache_details_file))
217             {
218             my @stopwords = (STOPWORDS => $options->{stopwords_file}) if $options->{stopwords_file} ;
219              
220             my $indexer = Search::Indexer::Incremental::MD5::Indexer->new
221             (
222             INDEX_DIRECTORY => $options->{index_directory},
223             USE_POSITIONS => 0,
224             Search::Indexer::Incremental::MD5::Language::Perl::get_perl_word_regex_and_stopwords(),
225             @stopwords,
226             ) ;
227              
228             my ($modules_in_repository, $modules_up_to_date, $modules_out_of_date) = scan_index($cpan_mini, $indexer) ;
229              
230             remove_out_of_date_modules($indexer, $modules_out_of_date, $options) ;
231              
232             my %new_modules = grep { ! exists $modules_up_to_date->{$_} } keys %{$modules_in_repository};
233              
234             add_new_modules($indexer, $cpan_mini, \%new_modules, $options) ;
235              
236             if(-e $modules_details_file)
237             {
238             copy($modules_details_file, $cache_details_file) or carp "Warning: '$cache_details_file' creation failed: $!" ;
239             }
240             }
241              
242             return ;
243             }
244              
245             #----------------------------------------------------------------------------------------------------------
246              
247             sub index_needs_update
248             {
249              
250             =head2 ( )
251              
252             some code
253              
254             I
255              
256             =over 2
257              
258             =item * $ -
259              
260             =back
261              
262             I - Nothing
263              
264             I - None
265              
266             =cut
267              
268             my ($modules_details_file, $cache_details_file) = @_ ;
269              
270             my $index_need_update = 1 ;
271              
272             if(-e $modules_details_file && -e $cache_details_file)
273             {
274             if
275             (
276             Search::Indexer::Incremental::MD5::get_file_MD5($modules_details_file)
277             eq Search::Indexer::Incremental::MD5::get_file_MD5($cache_details_file)
278             )
279             {
280             $index_need_update = 0 ;
281             }
282             }
283            
284             return $index_need_update ;
285             }
286              
287             #----------------------------------------------------------------------------------------------------------
288              
289             sub scan_index
290             {
291              
292             =head2 ( )
293              
294             some code
295              
296             I
297              
298             =over 2
299              
300             =item * $ -
301              
302             =back
303              
304             I - Nothing
305              
306             I - None
307              
308             =cut
309              
310             my ($cpan_mini, $indexer) = @_ ;
311              
312             my %modules_in_repository
313             = map {chomp($_) ; $_ => 1}
314             File::Find::Rule
315             ->file()
316             ->name('*.tar.gz')
317             ->in($cpan_mini);
318              
319              
320             my (%modules_up_to_date, %indexed_modules_to_remove) ;
321              
322             $indexer->check_indexed_files
323             (
324             DONE_ONE_FILE_CALLBACK =>
325             sub
326             {
327             my ($file, $description, $file_info) = @_ ;
328            
329             if(exists $modules_in_repository{"$cpan_mini/$description"})
330             {
331             # we can't delete $modules_in_repository{$cpan_mini . $description} as
332             # it may contain multiple indexed files
333             $modules_up_to_date{"$cpan_mini/$description"}++ ;
334             }
335             else
336             {
337             $indexed_modules_to_remove{$description}{$file} = $file_info->{ID} ;
338             }
339             },
340             ) ;
341            
342             return (\%modules_in_repository, \%modules_up_to_date, \%indexed_modules_to_remove) ;
343             }
344              
345             #----------------------------------------------------------------------------------------------------------
346              
347             sub remove_out_of_date_modules
348             {
349              
350             =head2 ( )
351              
352             some code
353              
354             I
355              
356             =over 2
357              
358             =item * $ -
359              
360             =back
361              
362             I - Nothing
363              
364             I - None
365              
366             =cut
367              
368             my ($indexer, $modules_out_of_date, $options) = @_ ;
369              
370             my $t0_remove = time ;
371              
372             my $number_of_modules = scalar(keys %{$modules_out_of_date}) ;
373             my $module_index = 0 ;
374             my $total_number_of_files = 0 ;
375              
376             for my $module_to_remove(sort keys %{$modules_out_of_date})
377             {
378             my $t0_remove_module = time ;
379            
380             $module_index++ ;
381             print "-$module_to_remove\n" ;
382            
383             my $number_of_files_in_module = 0 ;
384            
385             for my $module_element (sort keys %{$modules_out_of_date->{$module_to_remove}})
386             {
387             (my $module_element_short = $module_element) =~ s{^/tmp/[^/]+/}[] ;
388            
389             $total_number_of_files++ ;
390             $number_of_files_in_module++ ;
391            
392             print "\t-$module_element_short\n" if $options->{verbose} ;
393            
394             $indexer->remove_document_with_id($modules_out_of_date->{$module_to_remove}{$module_element}) ;
395             }
396            
397             if ($options->{verbose})
398             {
399             printf
400             "\t[$module_index/$number_of_modules ($number_of_files_in_module) in %.3f s.]\n",
401             (time - $t0_remove_module) ;
402             }
403             }
404              
405             if ($options->{verbose})
406             {
407             printf "[Removed $total_number_of_files files in $number_of_modules modules in %.3f s.]\n", (time - $t0_remove) ;
408             }
409             }
410              
411             #----------------------------------------------------------------------------------------------------------
412              
413             sub add_new_modules
414             {
415              
416             =head2 ( )
417              
418             some code
419              
420             I
421              
422             =over 2
423              
424             =item * $ -
425              
426             =back
427              
428             I - Nothing
429              
430             I - None
431              
432             =cut
433              
434             my ($indexer, $cpan_mini, $new_modules, $options) = @_ ;
435              
436             my $module_index = 0 ;
437             my $total_number_of_files = 0 ;
438             my $number_of_modules = scalar(keys %{$new_modules}) ;
439              
440             my $one_warning = 0 ;
441             local $SIG{__WARN__} = get_sig_warn_sub(\$one_warning) ;
442              
443             my $t0_add_modules = time;
444              
445             for my $module (sort keys %{$new_modules})
446             {
447             my $t0_module = time ;
448            
449             $one_warning = 0 ;
450             $module_index++ ;
451            
452             (my $module_to_add_short = $module) =~ s{^$cpan_mini/}[] ;
453             print "+$module_to_add_short\n" ;
454              
455             my $directory = File::Temp->newdir() ;
456             my $extraction_directory = $directory->dirname;
457              
458             my $next_archive_item = Archive::Tar->iter($module, 1);
459              
460             while(my $item = $next_archive_item->())
461             {
462             my $item_name = $item->name() ;
463             $item->extract("$extraction_directory/$item_name")
464             or carp "Error: failed Extracting '$item_name' from '$module'!\n";
465             }
466            
467             my @files = File::Find::Rule->file()->name( '*.pod', '*.pl', '*.pm')->in($extraction_directory);
468              
469             my $number_of_files_in_module = scalar(@files) ;
470             $total_number_of_files += $number_of_files_in_module ;
471            
472             my $t0_index = time ;
473            
474             for my $file (@files)
475             {
476             (my $file_short = $file) =~ s{^/tmp/[^/]+/}{} ;
477             print "\t+$file_short\n" if ($options->{verbose}) ;
478            
479             $indexer->add_files
480             (
481             FILES => [map { {NAME => $_, DESCRIPTION => $module_to_add_short} } $file],
482             MAXIMUM_DOCUMENT_SIZE => $options->{maximum_document_size},
483             ) ;
484             }
485            
486             if ($options->{verbose})
487             {
488             printf
489             "\t[$module_index/$number_of_modules ($number_of_files_in_module) in "
490             . "%.3f s. (indexing: %.3f s.)]\n",
491             (time - $t0_module), (time - $t0_index) ;
492             }
493             }
494            
495             if ($options->{verbose})
496             {
497             print {*STDOUT}
498             pluralize("[Re-indexed $total_number_of_files file(s) in ", $total_number_of_files),
499             pluralize("$number_of_modules module(s) in ", $number_of_modules),
500             sprintf("%.3f s.]\n", (time - $t0_add_modules)) ;
501             }
502             }
503              
504             #----------------------------------------------------------------------------------------------------------
505              
506             sub get_sig_warn_sub
507             {
508              
509             =head2 ( )
510              
511             some code
512              
513             I
514              
515             =over 2
516              
517             =item * $ -
518              
519             =back
520              
521             I - Nothing
522              
523             I - None
524              
525             =cut
526              
527             my ($one_warning) = @_ ;
528              
529             return
530             sub
531             {
532             my ($warning) = @_ ;
533            
534             if
535             (
536             $warning =~ /^Invalid header block at offset unknown/
537             || $warning =~ /^Couldn't read chunk/
538             || $warning =~ /checksum error/
539             )
540             {
541             if(! $$one_warning)
542             {
543             print "\tInvalid Archive!\n" ;
544             $$one_warning++ ;
545             }
546             else
547             {
548             # ignore
549             }
550             }
551             else
552             {
553             if($warning =~ m~'/tmp/.+?/(.+)' is bigger than .+ bytes, skipping!~)
554             {
555             print "\tSkipping '$1', too big!\n" ;
556             }
557             else
558             {
559             warn $warning ;
560             }
561             }
562             } ;
563             }
564              
565             #-------------------------------------------------------------------------------
566              
567             1 ;
568              
569             =head1 BUGS AND LIMITATIONS
570              
571             None so far.
572              
573             =head1 AUTHOR
574              
575             Nadim ibn hamouda el Khemir
576             CPAN ID: NKH
577             mailto: nadim@cpan.org
578              
579             =head1 LICENSE AND COPYRIGHT
580              
581             This program is free software; you can redistribute
582             it and/or modify it under the same terms as Perl itself.
583              
584             =head1 SUPPORT
585              
586             You can find documentation for this module with the perldoc command.
587              
588             perldoc CPAN::Mini::Indexed
589              
590             You can also look for information at:
591              
592             =over 4
593              
594             =item * AnnoCPAN: Annotated CPAN documentation
595              
596             L
597              
598             =item * RT: CPAN's request tracker
599              
600             Please report any bugs or feature requests to L .
601              
602             We will be notified, and then you'll automatically be notified of progress on
603             your bug as we make changes.
604              
605             =item * Search CPAN
606              
607             L
608              
609             =back
610              
611             =head1 SEE ALSO
612              
613              
614             =cut