Bug 6435: (follow-up) rebuild_zebra -daemon option now smarter
[koha.git] / misc / migration_tools / rebuild_zebra.pl
1 #!/usr/bin/perl
2
3 use strict;
4 #use warnings; FIXME - Bug 2505
5
6 use C4::Context;
7 use Getopt::Long;
8 use File::Temp qw/ tempdir /;
9 use File::Path;
10 use C4::Biblio;
11 use C4::AuthoritiesMarc;
12 use C4::Items;
13 use Koha::RecordProcessor;
14 use XML::LibXML;
15
16 # script that checks zebradir structure & create directories & mandatory files if needed
17 #
18 #
19
20 $|=1; # flushes output
21 # If the cron job starts us in an unreadable dir, we will break without
22 # this.
23 chdir $ENV{HOME} if (!(-r '.'));
24 my $daemon_mode;
25 my $daemon_sleep = 5;
26 my $directory;
27 my $nosanitize;
28 my $skip_export;
29 my $keep_export;
30 my $skip_index;
31 my $reset;
32 my $biblios;
33 my $authorities;
34 my $noxml;
35 my $noshadow;
36 my $do_munge;
37 my $want_help;
38 my $as_xml;
39 my $process_zebraqueue;
40 my $do_not_clear_zebraqueue;
41 my $length;
42 my $where;
43 my $offset;
44 my $run_as_root;
45 my $run_user = (getpwuid($<))[0];
46
47 my $verbose_logging = 0;
48 my $zebraidx_log_opt = " -v none,fatal,warn ";
49 my $result = GetOptions(
50     'daemon'        => \$daemon_mode,
51     'sleep:i'       => \$daemon_sleep,
52     'd:s'           => \$directory,
53     'r|reset'       => \$reset,
54     's'             => \$skip_export,
55     'k'             => \$keep_export,
56     'I|skip-index'  => \$skip_index,
57     'nosanitize'    => \$nosanitize,
58     'b'             => \$biblios,
59     'noxml'         => \$noxml,
60     'w'             => \$noshadow,
61     'munge-config'  => \$do_munge,
62     'a'             => \$authorities,
63     'h|help'        => \$want_help,
64     'x'             => \$as_xml,
65     'y'             => \$do_not_clear_zebraqueue,
66     'z'             => \$process_zebraqueue,
67     'where:s'        => \$where,
68     'length:i'        => \$length,
69     'offset:i'      => \$offset,
70     'v+'             => \$verbose_logging,
71     'run-as-root'    => \$run_as_root,
72 );
73
74 if (not $result or $want_help) {
75     print_usage();
76     exit 0;
77 }
78
79 if( not defined $run_as_root and $run_user eq 'root') {
80     my $msg = "Warning: You are running this script as the user 'root'.\n";
81     $msg   .= "If this is intentional you must explicitly specify this using the -run-as-root switch\n";
82     $msg   .= "Please do '$0 --help' to see usage.\n";
83     die $msg;
84 }
85
86 if (not $biblios and not $authorities) {
87     my $msg = "Must specify -b or -a to reindex bibs or authorities\n";
88     $msg   .= "Please do '$0 --help' to see usage.\n";
89     die $msg;
90 }
91
92 if ( !$as_xml and $nosanitize ) {
93     my $msg = "Cannot specify both -no_xml and -nosanitize\n";
94     $msg   .= "Please do '$0 --help' to see usage.\n";
95     die $msg;
96 }
97
98 if ($process_zebraqueue and ($skip_export or $reset)) {
99     my $msg = "Cannot specify -r or -s if -z is specified\n";
100     $msg   .= "Please do '$0 --help' to see usage.\n";
101     die $msg;
102 }
103
104 if ($process_zebraqueue and $do_not_clear_zebraqueue) {
105     my $msg = "Cannot specify both -y and -z\n";
106     $msg   .= "Please do '$0 --help' to see usage.\n";
107     die $msg;
108 }
109
110 if ($reset) {
111     $noshadow = 1;
112 }
113
114 if ($noshadow) {
115     $noshadow = ' -n ';
116 }
117
118 if ($daemon_mode) {
119     # incompatible flags handled above: help, reset, and do_not_clear_zebraqueue
120     if ($skip_export or $keep_export or $skip_index or
121           $where or $length or $offset) {
122         my $msg = "Cannot specify -s, -k, -I, -where, -length, or -offset with -daemon.\n";
123         $msg   .= "Please do '$0 --help' to see usage.\n";
124         die $msg;
125     }
126     $authorities = 1;
127     $biblios = 1;
128     $process_zebraqueue = 1;
129 }
130
131 #  -v is for verbose, which seems backwards here because of how logging is set
132 #    on the CLI of zebraidx.  It works this way.  The default is to not log much
133 if ($verbose_logging >= 2) {
134     $zebraidx_log_opt = '-v none,fatal,warn,all';
135 }
136
137 my $use_tempdir = 0;
138 unless ($directory) {
139     $use_tempdir = 1;
140     $directory = tempdir(CLEANUP => ($keep_export ? 0 : 1));
141 }
142
143
144 my $biblioserverdir = C4::Context->zebraconfig('biblioserver')->{directory};
145 my $authorityserverdir = C4::Context->zebraconfig('authorityserver')->{directory};
146
147 my $kohadir = C4::Context->config('intranetdir');
148 my $bib_index_mode = C4::Context->config('zebra_bib_index_mode') || 'grs1';
149 my $auth_index_mode = C4::Context->config('zebra_auth_index_mode') || 'dom';
150
151 my $dbh = C4::Context->dbh;
152 my ($biblionumbertagfield,$biblionumbertagsubfield) = &GetMarcFromKohaField("biblio.biblionumber","");
153 my ($biblioitemnumbertagfield,$biblioitemnumbertagsubfield) = &GetMarcFromKohaField("biblioitems.biblioitemnumber","");
154
155 if ( $verbose_logging ) {
156     print "Zebra configuration information\n";
157     print "================================\n";
158     print "Zebra biblio directory      = $biblioserverdir\n";
159     print "Zebra authorities directory = $authorityserverdir\n";
160     print "Koha directory              = $kohadir\n";
161     print "BIBLIONUMBER in :     $biblionumbertagfield\$$biblionumbertagsubfield\n";
162     print "BIBLIOITEMNUMBER in : $biblioitemnumbertagfield\$$biblioitemnumbertagsubfield\n";
163     print "================================\n";
164 }
165
166 if ($do_munge) {
167     munge_config();
168 }
169
170 my $tester = XML::LibXML->new();
171
172 if ($daemon_mode) {
173     while (1) {
174         do_one_pass() if ( zebraqueue_not_empty() );
175         sleep $daemon_sleep;
176     }
177 } else {
178     do_one_pass();
179 }
180
181
182 if ( $verbose_logging ) {
183     print "====================\n";
184     print "CLEANING\n";
185     print "====================\n";
186 }
187 if ($keep_export) {
188     print "NOTHING cleaned : the export $directory has been kept.\n";
189     print "You can re-run this script with the -s ";
190     if ($use_tempdir) {
191         print " and -d $directory parameters";
192     } else {
193         print "parameter";
194     }
195     print "\n";
196     print "if you just want to rebuild zebra after changing the record.abs\n";
197     print "or another zebra config file\n";
198 } else {
199     unless ($use_tempdir) {
200         # if we're using a temporary directory
201         # created by File::Temp, it will be removed
202         # automatically.
203         rmtree($directory, 0, 1);
204         print "directory $directory deleted\n";
205     }
206 }
207
208 sub do_one_pass {
209     if ($authorities) {
210         index_records('authority', $directory, $skip_export, $skip_index, $process_zebraqueue, $as_xml, $noxml, $nosanitize, $do_not_clear_zebraqueue, $verbose_logging, $zebraidx_log_opt, $authorityserverdir);
211     } else {
212         print "skipping authorities\n" if ( $verbose_logging );
213     }
214
215     if ($biblios) {
216         index_records('biblio', $directory, $skip_export, $skip_index, $process_zebraqueue, $as_xml, $noxml, $nosanitize, $do_not_clear_zebraqueue, $verbose_logging, $zebraidx_log_opt, $biblioserverdir);
217     } else {
218         print "skipping biblios\n" if ( $verbose_logging );
219     }
220 }
221
222 # Check the zebra update queue and return true if there are records to process
223 # This routine will handle each of -ab, -a, or -b, but in practice we force
224 # -ab when in daemon mode.
225 sub zebraqueue_not_empty {
226     my $where_str;
227
228     if ($authorities && $biblios) {
229         $where_str = 'done = 0;';
230     } elsif ($biblios) {
231         $where_str = 'server = "biblioserver" AND done = 0;';
232     } else {
233         $where_str = 'server = "authorityserver" AND done = 0;';
234     }
235     my $query =
236       $dbh->prepare( 'SELECT COUNT(*) FROM zebraqueue WHERE ' . $where_str );
237
238     $query->execute;
239     my $count = $query->fetchrow_arrayref->[0];
240     print "queued records: $count\n" if $verbose_logging > 0;
241     return $count > 0;
242 }
243
244 # This checks to see if the zebra directories exist under the provided path.
245 # If they don't, then zebra is likely to spit the dummy. This returns true
246 # if the directories had to be created, false otherwise.
247 sub check_zebra_dirs {
248     my ($base) = shift() . '/';
249     my $needed_repairing = 0;
250     my @dirs = ( '', 'key', 'register', 'shadow', 'tmp' );
251     foreach my $dir (@dirs) {
252         my $bdir = $base . $dir;
253         if (! -d $bdir) {
254             $needed_repairing = 1;
255             mkdir $bdir || die "Unable to create '$bdir': $!\n";
256             print "$0: needed to create '$bdir'\n";
257         }
258     }
259     return $needed_repairing;
260 }   # ----------  end of subroutine check_zebra_dirs  ----------
261
262 sub index_records {
263     my ($record_type, $directory, $skip_export, $skip_index, $process_zebraqueue, $as_xml, $noxml, $nosanitize, $do_not_clear_zebraqueue, $verbose_logging, $zebraidx_log_opt, $server_dir) = @_;
264
265     my $num_records_exported = 0;
266     my $records_deleted;
267     my $need_reset = check_zebra_dirs($server_dir);
268     if ($need_reset) {
269         print "$0: found broken zebra server directories: forcing a rebuild\n";
270         $reset = 1;
271     }
272     if ($skip_export && $verbose_logging) {
273         print "====================\n";
274         print "SKIPPING $record_type export\n";
275         print "====================\n";
276     } else {
277         if ( $verbose_logging ) {
278             print "====================\n";
279             print "exporting $record_type\n";
280             print "====================\n";
281         }
282         mkdir "$directory" unless (-d $directory);
283         mkdir "$directory/$record_type" unless (-d "$directory/$record_type");
284         if ($process_zebraqueue) {
285             my $entries = select_zebraqueue_records($record_type, 'deleted');
286             mkdir "$directory/del_$record_type" unless (-d "$directory/del_$record_type");
287             $records_deleted = generate_deleted_marc_records($record_type, $entries, "$directory/del_$record_type", $as_xml);
288             mark_zebraqueue_batch_done($entries);
289             $entries = select_zebraqueue_records($record_type, 'updated');
290             mkdir "$directory/upd_$record_type" unless (-d "$directory/upd_$record_type");
291             $num_records_exported = export_marc_records_from_list($record_type,
292                                                                   $entries, "$directory/upd_$record_type", $as_xml, $noxml, $records_deleted);
293             mark_zebraqueue_batch_done($entries);
294         } else {
295             my $sth = select_all_records($record_type);
296             $num_records_exported = export_marc_records_from_sth($record_type, $sth, "$directory/$record_type", $as_xml, $noxml, $nosanitize);
297             unless ($do_not_clear_zebraqueue) {
298                 mark_all_zebraqueue_done($record_type);
299             }
300         }
301     }
302
303     #
304     # and reindexing everything
305     #
306     if ($skip_index) {
307         if ($verbose_logging) {
308             print "====================\n";
309             print "SKIPPING $record_type indexing\n";
310             print "====================\n";
311         }
312     } else {
313         if ( $verbose_logging ) {
314             print "====================\n";
315             print "REINDEXING zebra\n";
316             print "====================\n";
317         }
318         my $record_fmt = ($as_xml) ? 'marcxml' : 'iso2709' ;
319         if ($process_zebraqueue) {
320             do_indexing($record_type, 'adelete', "$directory/del_$record_type", $reset, $noshadow, $record_fmt, $zebraidx_log_opt)
321                 if %$records_deleted;
322             do_indexing($record_type, 'update', "$directory/upd_$record_type", $reset, $noshadow, $record_fmt, $zebraidx_log_opt)
323                 if $num_records_exported;
324         } else {
325             do_indexing($record_type, 'update', "$directory/$record_type", $reset, $noshadow, $record_fmt, $zebraidx_log_opt)
326                 if ($num_records_exported or $skip_export);
327         }
328     }
329 }
330
331
332 sub select_zebraqueue_records {
333     my ($record_type, $update_type) = @_;
334
335     my $server = ($record_type eq 'biblio') ? 'biblioserver' : 'authorityserver';
336     my $op = ($update_type eq 'deleted') ? 'recordDelete' : 'specialUpdate';
337
338     my $sth = $dbh->prepare("SELECT id, biblio_auth_number
339                              FROM zebraqueue
340                              WHERE server = ?
341                              AND   operation = ?
342                              AND   done = 0
343                              ORDER BY id DESC");
344     $sth->execute($server, $op);
345     my $entries = $sth->fetchall_arrayref({});
346 }
347
348 sub mark_all_zebraqueue_done {
349     my ($record_type) = @_;
350
351     my $server = ($record_type eq 'biblio') ? 'biblioserver' : 'authorityserver';
352
353     my $sth = $dbh->prepare("UPDATE zebraqueue SET done = 1
354                              WHERE server = ?
355                              AND done = 0");
356     $sth->execute($server);
357 }
358
359 sub mark_zebraqueue_batch_done {
360     my ($entries) = @_;
361
362     $dbh->{AutoCommit} = 0;
363     my $sth = $dbh->prepare("UPDATE zebraqueue SET done = 1 WHERE id = ?");
364     $dbh->commit();
365     foreach my $id (map { $_->{id} } @$entries) {
366         $sth->execute($id);
367     }
368     $dbh->{AutoCommit} = 1;
369 }
370
371 sub select_all_records {
372     my $record_type = shift;
373     return ($record_type eq 'biblio') ? select_all_biblios() : select_all_authorities();
374 }
375
376 sub select_all_authorities {
377     my $strsth=qq{SELECT authid FROM auth_header};
378     $strsth.=qq{ WHERE $where } if ($where);
379     $strsth.=qq{ LIMIT $length } if ($length && !$offset);
380     $strsth.=qq{ LIMIT $offset,$length } if ($length && $offset);
381     my $sth = $dbh->prepare($strsth);
382     $sth->execute();
383     return $sth;
384 }
385
386 sub select_all_biblios {
387     my $strsth = qq{ SELECT biblionumber FROM biblioitems };
388     $strsth.=qq{ WHERE $where } if ($where);
389     $strsth.=qq{ LIMIT $length } if ($length && !$offset);
390     $strsth.=qq{ LIMIT $offset,$length } if ($offset);
391     my $sth = $dbh->prepare($strsth);
392     $sth->execute();
393     return $sth;
394 }
395
396 sub include_xml_wrapper {
397     my $as_xml = shift;
398     my $record_type = shift;
399
400     return 0 unless $as_xml;
401     return 1 if $record_type eq 'biblio' and $bib_index_mode eq 'dom';
402     return 1 if $record_type eq 'authority' and $auth_index_mode eq 'dom';
403     return 0;
404
405 }
406
407 sub export_marc_records_from_sth {
408     my ($record_type, $sth, $directory, $as_xml, $noxml, $nosanitize) = @_;
409
410     my $num_exported = 0;
411     open my $fh, '>:encoding(UTF-8) ', "$directory/exported_records" or die $!;
412     if (include_xml_wrapper($as_xml, $record_type)) {
413         # include XML declaration and root element
414         print {$fh} '<?xml version="1.0" encoding="UTF-8"?><collection>';
415     }
416     my $i = 0;
417     my ( $itemtag, $itemsubfield ) = GetMarcFromKohaField("items.itemnumber",'');
418     while (my ($record_number) = $sth->fetchrow_array) {
419         print "." if ( $verbose_logging );
420         print "\r$i" unless ($i++ %100 or !$verbose_logging);
421         if ( $nosanitize ) {
422             my $marcxml = $record_type eq 'biblio'
423                           ? GetXmlBiblio( $record_number )
424                           : GetAuthorityXML( $record_number );
425             if ($record_type eq 'biblio'){
426                 my @items = GetItemsInfo($record_number);
427                 if (@items){
428                     my $record = MARC::Record->new;
429                     $record->encoding('UTF-8');
430                     my @itemsrecord;
431                     foreach my $item (@items){
432                         my $record = Item2Marc($item, $record_number);
433                         push @itemsrecord, $record->field($itemtag);
434                     }
435                     $record->insert_fields_ordered(@itemsrecord);
436                     my $itemsxml = $record->as_xml_record();
437                     $marcxml =
438                         substr($marcxml, 0, length($marcxml)-10) .
439                         substr($itemsxml, index($itemsxml, "</leader>\n", 0) + 10);
440                 }
441             }
442             # extra test to ensure that result is valid XML; otherwise
443             # Zebra won't parse it in DOM mode
444             eval {
445                 my $doc = $tester->parse_string($marcxml);
446             };
447             if ($@) {
448                 warn "Error exporting record $record_number ($record_type): $@\n";
449                 next;
450             }
451             if ( $marcxml ) {
452                 $marcxml =~ s!<\?xml version="1.0" encoding="UTF-8"\?>\n!!;
453                 print {$fh} $marcxml;
454                 $num_exported++;
455             }
456             next;
457         }
458         my ($marc) = get_corrected_marc_record($record_type, $record_number, $noxml);
459         if (defined $marc) {
460             eval {
461                 my $rec;
462                 if ($as_xml) {
463                     $rec = $marc->as_xml_record(C4::Context->preference('marcflavour'));
464                     eval {
465                         my $doc = $tester->parse_string($rec);
466                     };
467                     if ($@) {
468                         die "invalid XML: $@";
469                     }
470                     $rec =~ s!<\?xml version="1.0" encoding="UTF-8"\?>\n!!;
471                 } else {
472                     $rec = $marc->as_usmarc();
473                 }
474                 print {$fh} $rec;
475                 $num_exported++;
476             };
477             if ($@) {
478                 warn "Error exporting record $record_number ($record_type) ".($noxml ? "not XML" : "XML");
479                 warn "... specific error is $@" if $verbose_logging;
480             }
481         }
482     }
483     print "\nRecords exported: $num_exported\n" if ( $verbose_logging );
484     print {$fh} '</collection>' if (include_xml_wrapper($as_xml, $record_type));
485     close $fh;
486     return $num_exported;
487 }
488
489 sub export_marc_records_from_list {
490     my ($record_type, $entries, $directory, $as_xml, $noxml, $records_deleted) = @_;
491
492     my $num_exported = 0;
493     open my $fh, '>:encoding(UTF-8)', "$directory/exported_records" or die $!;
494     if (include_xml_wrapper($as_xml, $record_type)) {
495         # include XML declaration and root element
496         print {$fh} '<?xml version="1.0" encoding="UTF-8"?><collection>';
497     }
498     my $i = 0;
499
500     # Skip any deleted records. We check for this anyway, but this reduces error spam
501     my %found = %$records_deleted;
502     foreach my $record_number ( map { $_->{biblio_auth_number} }
503                                 grep { !$found{ $_->{biblio_auth_number} }++ }
504                                 @$entries ) {
505         print "." if ( $verbose_logging );
506         print "\r$i" unless ($i++ %100 or !$verbose_logging);
507         my ($marc) = get_corrected_marc_record($record_type, $record_number, $noxml);
508         if (defined $marc) {
509             eval {
510                 my $rec;
511                 if ($as_xml) {
512                     $rec = $marc->as_xml_record(C4::Context->preference('marcflavour'));
513                     $rec =~ s!<\?xml version="1.0" encoding="UTF-8"\?>\n!!;
514                 } else {
515                     $rec = $marc->as_usmarc();
516                 }
517                 print {$fh} $rec;
518                 $num_exported++;
519             };
520             if ($@) {
521               warn "Error exporting record $record_number ($record_type) ".($noxml ? "not XML" : "XML");
522             }
523         }
524     }
525     print "\nRecords exported: $num_exported\n" if ( $verbose_logging );
526     print {$fh} '</collection>' if (include_xml_wrapper($as_xml, $record_type));
527     close $fh;
528     return $num_exported;
529 }
530
531 sub generate_deleted_marc_records {
532     my ($record_type, $entries, $directory, $as_xml) = @_;
533
534     my $records_deleted = {};
535     open my $fh, '>:encoding(UTF-8)', "$directory/exported_records" or die $!;
536     if (include_xml_wrapper($as_xml, $record_type)) {
537         # include XML declaration and root element
538         print {$fh} '<?xml version="1.0" encoding="UTF-8"?><collection>';
539     }
540     my $i = 0;
541     foreach my $record_number (map { $_->{biblio_auth_number} } @$entries ) {
542         print "\r$i" unless ($i++ %100 or !$verbose_logging);
543         print "." if ( $verbose_logging );
544
545         my $marc = MARC::Record->new();
546         if ($record_type eq 'biblio') {
547             fix_biblio_ids($marc, $record_number, $record_number);
548         } else {
549             fix_authority_id($marc, $record_number);
550         }
551         if (C4::Context->preference("marcflavour") eq "UNIMARC") {
552             fix_unimarc_100($marc);
553         }
554
555         my $rec;
556         if ($as_xml) {
557             $rec = $marc->as_xml_record(C4::Context->preference('marcflavour'));
558             $rec =~ s!<\?xml version="1.0" encoding="UTF-8"\?>\n!!;
559         } else {
560             $rec = $marc->as_usmarc();
561         }
562         print {$fh} $rec;
563
564         $records_deleted->{$record_number} = 1;
565     }
566     print "\nRecords exported: $i\n" if ( $verbose_logging );
567     print {$fh} '</collection>' if (include_xml_wrapper($as_xml, $record_type));
568     close $fh;
569     return $records_deleted;
570
571
572 }
573
574 sub get_corrected_marc_record {
575     my ($record_type, $record_number, $noxml) = @_;
576
577     my $marc = get_raw_marc_record($record_type, $record_number, $noxml);
578
579     if (defined $marc) {
580         fix_leader($marc);
581         if ($record_type eq 'authority') {
582             fix_authority_id($marc, $record_number);
583         } elsif ($record_type eq 'biblio' && C4::Context->preference('IncludeSeeFromInSearches')) {
584             my $normalizer = Koha::RecordProcessor->new( { filters => 'EmbedSeeFromHeadings' } );
585             $marc = $normalizer->process($marc);
586         }
587         if (C4::Context->preference("marcflavour") eq "UNIMARC") {
588             fix_unimarc_100($marc);
589         }
590     }
591
592     return $marc;
593 }
594
595 sub get_raw_marc_record {
596     my ($record_type, $record_number, $noxml) = @_;
597
598     my $marc;
599     if ($record_type eq 'biblio') {
600         if ($noxml) {
601             my $fetch_sth = $dbh->prepare_cached("SELECT marc FROM biblioitems WHERE biblionumber = ?");
602             $fetch_sth->execute($record_number);
603             if (my ($blob) = $fetch_sth->fetchrow_array) {
604                 $marc = MARC::Record->new_from_usmarc($blob);
605                 unless ($marc) {
606                     warn "error creating MARC::Record from $blob";
607                 }
608             }
609             # failure to find a bib is not a problem -
610             # a delete could have been done before
611             # trying to process a record update
612
613             $fetch_sth->finish();
614             return unless $marc;
615         } else {
616             eval { $marc = GetMarcBiblio($record_number, 1); };
617             if ($@ || !$marc) {
618                 # here we do warn since catching an exception
619                 # means that the bib was found but failed
620                 # to be parsed
621                 warn "error retrieving biblio $record_number";
622                 return;
623             }
624         }
625     } else {
626         eval { $marc = GetAuthority($record_number); };
627         if ($@) {
628             warn "error retrieving authority $record_number";
629             return;
630         }
631     }
632     return $marc;
633 }
634
635 sub fix_leader {
636     # FIXME - this routine is suspect
637     # It blanks the Leader/00-05 and Leader/12-16 to
638     # force them to be recalculated correct when
639     # the $marc->as_usmarc() or $marc->as_xml() is called.
640     # But why is this necessary?  It would be a serious bug
641     # in MARC::Record (definitely) and MARC::File::XML (arguably)
642     # if they are emitting incorrect leader values.
643     my $marc = shift;
644
645     my $leader = $marc->leader;
646     substr($leader,  0, 5) = '     ';
647     substr($leader, 10, 7) = '22     ';
648     $marc->leader(substr($leader, 0, 24));
649 }
650
651 sub fix_biblio_ids {
652     # FIXME - it is essential to ensure that the biblionumber is present,
653     #         otherwise, Zebra will choke on the record.  However, this
654     #         logic belongs in the relevant C4::Biblio APIs.
655     my $marc = shift;
656     my $biblionumber = shift;
657     my $biblioitemnumber;
658     if (@_) {
659         $biblioitemnumber = shift;
660     } else {
661         my $sth = $dbh->prepare(
662             "SELECT biblioitemnumber FROM biblioitems WHERE biblionumber=?");
663         $sth->execute($biblionumber);
664         ($biblioitemnumber) = $sth->fetchrow_array;
665         $sth->finish;
666         unless ($biblioitemnumber) {
667             warn "failed to get biblioitemnumber for biblio $biblionumber";
668             return 0;
669         }
670     }
671
672     # FIXME - this is cheating on two levels
673     # 1. C4::Biblio::_koha_marc_update_bib_ids is meant to be an internal function
674     # 2. Making sure that the biblionumber and biblioitemnumber are correct and
675     #    present in the MARC::Record object ought to be part of GetMarcBiblio.
676     #
677     # On the other hand, this better for now than what rebuild_zebra.pl used to
678     # do, which was duplicate the code for inserting the biblionumber
679     # and biblioitemnumber
680     C4::Biblio::_koha_marc_update_bib_ids($marc, '', $biblionumber, $biblioitemnumber);
681
682     return 1;
683 }
684
685 sub fix_authority_id {
686     # FIXME - as with fix_biblio_ids, the authid must be present
687     #         for Zebra's sake.  However, this really belongs
688     #         in C4::AuthoritiesMarc.
689     my ($marc, $authid) = @_;
690     unless ($marc->field('001') and $marc->field('001')->data() eq $authid){
691         $marc->delete_field($marc->field('001'));
692         $marc->insert_fields_ordered(MARC::Field->new('001',$authid));
693     }
694 }
695
696 sub fix_unimarc_100 {
697     # FIXME - again, if this is necessary, it belongs in C4::AuthoritiesMarc.
698     my $marc = shift;
699
700     my $string;
701     if ( length($marc->subfield( 100, "a" )) == 36 ) {
702         $string = $marc->subfield( 100, "a" );
703         my $f100 = $marc->field(100);
704         $marc->delete_field($f100);
705     }
706     else {
707         $string = POSIX::strftime( "%Y%m%d", localtime );
708         $string =~ s/\-//g;
709         $string = sprintf( "%-*s", 35, $string );
710     }
711     substr( $string, 22, 6, "frey50" );
712     unless ( length($marc->subfield( 100, "a" )) == 36 ) {
713         $marc->delete_field($marc->field(100));
714         $marc->insert_grouped_field(MARC::Field->new( 100, "", "", "a" => $string ));
715     }
716 }
717
718 sub do_indexing {
719     my ($record_type, $op, $record_dir, $reset_index, $noshadow, $record_format, $zebraidx_log_opt) = @_;
720
721     my $zebra_server  = ($record_type eq 'biblio') ? 'biblioserver' : 'authorityserver';
722     my $zebra_db_name = ($record_type eq 'biblio') ? 'biblios' : 'authorities';
723     my $zebra_config  = C4::Context->zebraconfig($zebra_server)->{'config'};
724     my $zebra_db_dir  = C4::Context->zebraconfig($zebra_server)->{'directory'};
725
726     system("zebraidx -c $zebra_config $zebraidx_log_opt -g $record_format -d $zebra_db_name init") if $reset_index;
727     system("zebraidx -c $zebra_config $zebraidx_log_opt $noshadow -g $record_format -d $zebra_db_name $op $record_dir");
728     system("zebraidx -c $zebra_config $zebraidx_log_opt -g $record_format -d $zebra_db_name commit") unless $noshadow;
729
730 }
731
732 sub print_usage {
733     print <<_USAGE_;
734 $0: reindex MARC bibs and/or authorities in Zebra.
735
736 Use this batch job to reindex all biblio or authority
737 records in your Koha database.
738
739 Parameters:
740
741     -b                      index bibliographic records
742
743     -a                      index authority records
744
745     -daemon                 Run in daemon mode.  The program will loop checking
746                             for entries on the zebraqueue table, processing
747                             them incrementally if present, and then sleep
748                             for a few seconds before repeating the process
749                             Checking the zebraqueue table is done with a cheap
750                             SQL query.  This allows for near realtime update of
751                             the zebra search index with low system overhead.
752                             Use -sleep to control the checking interval.
753
754                             Daemon mode implies -z, -a, -b.  The program will
755                             refuse to start if options are present that do not
756                             make sense while running as an incremental update
757                             daemon (e.g. -r or -offset).
758
759     -sleep 10               Seconds to sleep between checks of the zebraqueue
760                             table in daemon mode.  The default is 5 seconds.
761
762     -z                      select only updated and deleted
763                             records marked in the zebraqueue
764                             table.  Cannot be used with -r
765                             or -s.
766
767     -r                      clear Zebra index before
768                             adding records to index. Implies -w.
769
770     -d                      Temporary directory for indexing.
771                             If not specified, one is automatically
772                             created.  The export directory
773                             is automatically deleted unless
774                             you supply the -k switch.
775
776     -k                      Do not delete export directory.
777
778     -s                      Skip export.  Used if you have
779                             already exported the records
780                             in a previous run.
781
782     -noxml                  index from ISO MARC blob
783                             instead of MARC XML.  This
784                             option is recommended only
785                             for advanced user.
786
787     -x                      export and index as xml instead of is02709 (biblios only).
788                             use this if you might have records > 99,999 chars,
789
790     -nosanitize             export biblio/authority records directly from DB marcxml
791                             field without sanitizing records. It speed up
792                             dump process but could fail if DB contains badly
793                             encoded records. Works only with -x,
794
795     -w                      skip shadow indexing for this batch
796
797     -y                      do NOT clear zebraqueue after indexing; normally,
798                             after doing batch indexing, zebraqueue should be
799                             marked done for the affected record type(s) so that
800                             a running zebraqueue_daemon doesn't try to reindex
801                             the same records - specify -y to override this.
802                             Cannot be used with -z.
803
804     -v                      increase the amount of logging.  Normally only
805                             warnings and errors from the indexing are shown.
806                             Use log level 2 (-v -v) to include all Zebra logs.
807
808     --length   1234         how many biblio you want to export
809     --offset 1243           offset you want to start to
810                                 example: --offset 500 --length=500 will result in a LIMIT 500,1000 (exporting 1000 records, starting by the 500th one)
811                                 note that the numbers are NOT related to biblionumber, that's the intended behaviour.
812     --where                 let you specify a WHERE query, like itemtype='BOOK'
813                             or something like that
814
815     --munge-config          Deprecated option to try
816                             to fix Zebra config files.
817
818     --run-as-root           explicitily allow script to run as 'root' user
819
820     --help or -h            show this message.
821 _USAGE_
822 }
823
824 # FIXME: the following routines are deprecated and
825 # will be removed once it is determined whether
826 # a script to fix Zebra configuration files is
827 # actually needed.
828 sub munge_config {
829 #
830 # creating zebra-biblios.cfg depending on system
831 #
832
833 # getting zebraidx directory
834 my $zebraidxdir;
835 foreach (qw(/usr/local/bin/zebraidx
836         /opt/bin/zebraidx
837         /usr/bin/zebraidx
838         )) {
839     if ( -f $_ ) {
840         $zebraidxdir=$_;
841     }
842 }
843
844 unless ($zebraidxdir) {
845     print qq|
846     ERROR: could not find zebraidx directory
847     ERROR: Either zebra is not installed,
848     ERROR: or it's in a directory I don't checked.
849     ERROR: do a which zebraidx and edit this file to add the result you get
850 |;
851     exit;
852 }
853 $zebraidxdir =~ s/\/bin\/.*//;
854 print "Info : zebra is in $zebraidxdir \n";
855
856 # getting modules directory
857 my $modulesdir;
858 foreach (qw(/usr/local/lib/idzebra-2.0/modules/mod-grs-xml.so
859             /usr/local/lib/idzebra/modules/mod-grs-xml.so
860             /usr/lib/idzebra/modules/mod-grs-xml.so
861             /usr/lib/idzebra-2.0/modules/mod-grs-xml.so
862         )) {
863     if ( -f $_ ) {
864         $modulesdir=$_;
865     }
866 }
867
868 unless ($modulesdir) {
869     print qq|
870     ERROR: could not find mod-grs-xml.so directory
871     ERROR: Either zebra is not properly compiled (libxml2 is not setup and you don t have mod-grs-xml.so,
872     ERROR: or it's in a directory I don't checked.
873     ERROR: find where mod-grs-xml.so is and edit this file to add the result you get
874 |;
875     exit;
876 }
877 $modulesdir =~ s/\/modules\/.*//;
878 print "Info: zebra modules dir : $modulesdir\n";
879
880 # getting tab directory
881 my $tabdir;
882 foreach (qw(/usr/local/share/idzebra/tab/explain.att
883             /usr/local/share/idzebra-2.0/tab/explain.att
884             /usr/share/idzebra/tab/explain.att
885             /usr/share/idzebra-2.0/tab/explain.att
886         )) {
887     if ( -f $_ ) {
888         $tabdir=$_;
889     }
890 }
891
892 unless ($tabdir) {
893     print qq|
894     ERROR: could not find explain.att directory
895     ERROR: Either zebra is not properly compiled,
896     ERROR: or it's in a directory I don't checked.
897     ERROR: find where explain.att is and edit this file to add the result you get
898 |;
899     exit;
900 }
901 $tabdir =~ s/\/tab\/.*//;
902 print "Info: tab dir : $tabdir\n";
903
904 #
905 # AUTHORITIES creating directory structure
906 #
907 my $created_dir_or_file = 0;
908 if ($authorities) {
909     if ( $verbose_logging ) {
910         print "====================\n";
911         print "checking directories & files for authorities\n";
912         print "====================\n";
913     }
914     unless (-d "$authorityserverdir") {
915         system("mkdir -p $authorityserverdir");
916         print "Info: created $authorityserverdir\n";
917         $created_dir_or_file++;
918     }
919     unless (-d "$authorityserverdir/lock") {
920         mkdir "$authorityserverdir/lock";
921         print "Info: created $authorityserverdir/lock\n";
922         $created_dir_or_file++;
923     }
924     unless (-d "$authorityserverdir/register") {
925         mkdir "$authorityserverdir/register";
926         print "Info: created $authorityserverdir/register\n";
927         $created_dir_or_file++;
928     }
929     unless (-d "$authorityserverdir/shadow") {
930         mkdir "$authorityserverdir/shadow";
931         print "Info: created $authorityserverdir/shadow\n";
932         $created_dir_or_file++;
933     }
934     unless (-d "$authorityserverdir/tab") {
935         mkdir "$authorityserverdir/tab";
936         print "Info: created $authorityserverdir/tab\n";
937         $created_dir_or_file++;
938     }
939     unless (-d "$authorityserverdir/key") {
940         mkdir "$authorityserverdir/key";
941         print "Info: created $authorityserverdir/key\n";
942         $created_dir_or_file++;
943     }
944
945     unless (-d "$authorityserverdir/etc") {
946         mkdir "$authorityserverdir/etc";
947         print "Info: created $authorityserverdir/etc\n";
948         $created_dir_or_file++;
949     }
950
951     #
952     # AUTHORITIES : copying mandatory files
953     #
954     # the record model, depending on marc flavour
955     unless (-f "$authorityserverdir/tab/record.abs") {
956         if (C4::Context->preference("marcflavour") eq "UNIMARC") {
957             system("cp -f $kohadir/etc/zebradb/marc_defs/unimarc/authorities/record.abs $authorityserverdir/tab/record.abs");
958             print "Info: copied record.abs for UNIMARC\n";
959         } else {
960             system("cp -f $kohadir/etc/zebradb/marc_defs/marc21/authorities/record.abs $authorityserverdir/tab/record.abs");
961             print "Info: copied record.abs for USMARC\n";
962         }
963         $created_dir_or_file++;
964     }
965     unless (-f "$authorityserverdir/tab/sort-string-utf.chr") {
966         system("cp -f $kohadir/etc/zebradb/lang_defs/fr/sort-string-utf.chr $authorityserverdir/tab/sort-string-utf.chr");
967         print "Info: copied sort-string-utf.chr\n";
968         $created_dir_or_file++;
969     }
970     unless (-f "$authorityserverdir/tab/word-phrase-utf.chr") {
971         system("cp -f $kohadir/etc/zebradb/lang_defs/fr/sort-string-utf.chr $authorityserverdir/tab/word-phrase-utf.chr");
972         print "Info: copied word-phase-utf.chr\n";
973         $created_dir_or_file++;
974     }
975     unless (-f "$authorityserverdir/tab/auth1.att") {
976         system("cp -f $kohadir/etc/zebradb/authorities/etc/bib1.att $authorityserverdir/tab/auth1.att");
977         print "Info: copied auth1.att\n";
978         $created_dir_or_file++;
979     }
980     unless (-f "$authorityserverdir/tab/default.idx") {
981         system("cp -f $kohadir/etc/zebradb/etc/default.idx $authorityserverdir/tab/default.idx");
982         print "Info: copied default.idx\n";
983         $created_dir_or_file++;
984     }
985
986     unless (-f "$authorityserverdir/etc/ccl.properties") {
987 #         system("cp -f $kohadir/etc/zebradb/ccl.properties ".C4::Context->zebraconfig('authorityserver')->{ccl2rpn});
988         system("cp -f $kohadir/etc/zebradb/ccl.properties $authorityserverdir/etc/ccl.properties");
989         print "Info: copied ccl.properties\n";
990         $created_dir_or_file++;
991     }
992     unless (-f "$authorityserverdir/etc/pqf.properties") {
993 #         system("cp -f $kohadir/etc/zebradb/pqf.properties ".C4::Context->zebraconfig('authorityserver')->{ccl2rpn});
994         system("cp -f $kohadir/etc/zebradb/pqf.properties $authorityserverdir/etc/pqf.properties");
995         print "Info: copied pqf.properties\n";
996         $created_dir_or_file++;
997     }
998
999     #
1000     # AUTHORITIES : copying mandatory files
1001     #
1002     unless (-f C4::Context->zebraconfig('authorityserver')->{config}) {
1003     open my $zd, '>:encoding(UTF-8)' ,C4::Context->zebraconfig('authorityserver')->{config};
1004     print {$zd} "
1005 # generated by KOHA/misc/migration_tools/rebuild_zebra.pl
1006 profilePath:\${srcdir:-.}:$authorityserverdir/tab/:$tabdir/tab/:\${srcdir:-.}/tab/
1007
1008 encoding: UTF-8
1009 # Files that describe the attribute sets supported.
1010 attset: auth1.att
1011 attset: explain.att
1012 attset: gils.att
1013
1014 modulePath:$modulesdir/modules/
1015 # Specify record type
1016 iso2709.recordType:grs.marcxml.record
1017 recordType:grs.xml
1018 recordId: (auth1,Local-Number)
1019 storeKeys:1
1020 storeData:1
1021
1022
1023 # Lock File Area
1024 lockDir: $authorityserverdir/lock
1025 perm.anonymous:r
1026 perm.kohaadmin:rw
1027 register: $authorityserverdir/register:4G
1028 shadow: $authorityserverdir/shadow:4G
1029
1030 # Temp File area for result sets
1031 setTmpDir: $authorityserverdir/tmp
1032
1033 # Temp File area for index program
1034 keyTmpDir: $authorityserverdir/key
1035
1036 # Approx. Memory usage during indexing
1037 memMax: 40M
1038 rank:rank-1
1039     ";
1040         print "Info: creating zebra-authorities.cfg\n";
1041         $created_dir_or_file++;
1042     }
1043
1044     if ($created_dir_or_file) {
1045         print "Info: created : $created_dir_or_file directories & files\n";
1046     } else {
1047         print "Info: file & directories OK\n";
1048     }
1049
1050 }
1051 if ($biblios) {
1052     if ( $verbose_logging ) {
1053         print "====================\n";
1054         print "checking directories & files for biblios\n";
1055         print "====================\n";
1056     }
1057
1058     #
1059     # BIBLIOS : creating directory structure
1060     #
1061     unless (-d "$biblioserverdir") {
1062         system("mkdir -p $biblioserverdir");
1063         print "Info: created $biblioserverdir\n";
1064         $created_dir_or_file++;
1065     }
1066     unless (-d "$biblioserverdir/lock") {
1067         mkdir "$biblioserverdir/lock";
1068         print "Info: created $biblioserverdir/lock\n";
1069         $created_dir_or_file++;
1070     }
1071     unless (-d "$biblioserverdir/register") {
1072         mkdir "$biblioserverdir/register";
1073         print "Info: created $biblioserverdir/register\n";
1074         $created_dir_or_file++;
1075     }
1076     unless (-d "$biblioserverdir/shadow") {
1077         mkdir "$biblioserverdir/shadow";
1078         print "Info: created $biblioserverdir/shadow\n";
1079         $created_dir_or_file++;
1080     }
1081     unless (-d "$biblioserverdir/tab") {
1082         mkdir "$biblioserverdir/tab";
1083         print "Info: created $biblioserverdir/tab\n";
1084         $created_dir_or_file++;
1085     }
1086     unless (-d "$biblioserverdir/key") {
1087         mkdir "$biblioserverdir/key";
1088         print "Info: created $biblioserverdir/key\n";
1089         $created_dir_or_file++;
1090     }
1091     unless (-d "$biblioserverdir/etc") {
1092         mkdir "$biblioserverdir/etc";
1093         print "Info: created $biblioserverdir/etc\n";
1094         $created_dir_or_file++;
1095     }
1096
1097     #
1098     # BIBLIOS : copying mandatory files
1099     #
1100     # the record model, depending on marc flavour
1101     unless (-f "$biblioserverdir/tab/record.abs") {
1102         if (C4::Context->preference("marcflavour") eq "UNIMARC") {
1103             system("cp -f $kohadir/etc/zebradb/marc_defs/unimarc/biblios/record.abs $biblioserverdir/tab/record.abs");
1104             print "Info: copied record.abs for UNIMARC\n";
1105         } else {
1106             system("cp -f $kohadir/etc/zebradb/marc_defs/marc21/biblios/record.abs $biblioserverdir/tab/record.abs");
1107             print "Info: copied record.abs for USMARC\n";
1108         }
1109         $created_dir_or_file++;
1110     }
1111     unless (-f "$biblioserverdir/tab/sort-string-utf.chr") {
1112         system("cp -f $kohadir/etc/zebradb/lang_defs/fr/sort-string-utf.chr $biblioserverdir/tab/sort-string-utf.chr");
1113         print "Info: copied sort-string-utf.chr\n";
1114         $created_dir_or_file++;
1115     }
1116     unless (-f "$biblioserverdir/tab/word-phrase-utf.chr") {
1117         system("cp -f $kohadir/etc/zebradb/lang_defs/fr/sort-string-utf.chr $biblioserverdir/tab/word-phrase-utf.chr");
1118         print "Info: copied word-phase-utf.chr\n";
1119         $created_dir_or_file++;
1120     }
1121     unless (-f "$biblioserverdir/tab/bib1.att") {
1122         system("cp -f $kohadir/etc/zebradb/biblios/etc/bib1.att $biblioserverdir/tab/bib1.att");
1123         print "Info: copied bib1.att\n";
1124         $created_dir_or_file++;
1125     }
1126     unless (-f "$biblioserverdir/tab/default.idx") {
1127         system("cp -f $kohadir/etc/zebradb/etc/default.idx $biblioserverdir/tab/default.idx");
1128         print "Info: copied default.idx\n";
1129         $created_dir_or_file++;
1130     }
1131     unless (-f "$biblioserverdir/etc/ccl.properties") {
1132 #         system("cp -f $kohadir/etc/zebradb/ccl.properties ".C4::Context->zebraconfig('biblioserver')->{ccl2rpn});
1133         system("cp -f $kohadir/etc/zebradb/ccl.properties $biblioserverdir/etc/ccl.properties");
1134         print "Info: copied ccl.properties\n";
1135         $created_dir_or_file++;
1136     }
1137     unless (-f "$biblioserverdir/etc/pqf.properties") {
1138 #         system("cp -f $kohadir/etc/zebradb/pqf.properties ".C4::Context->zebraconfig('biblioserver')->{ccl2rpn});
1139         system("cp -f $kohadir/etc/zebradb/pqf.properties $biblioserverdir/etc/pqf.properties");
1140         print "Info: copied pqf.properties\n";
1141         $created_dir_or_file++;
1142     }
1143
1144     #
1145     # BIBLIOS : copying mandatory files
1146     #
1147     unless (-f C4::Context->zebraconfig('biblioserver')->{config}) {
1148     open my $zd, '>:encoding(UTF-8)', C4::Context->zebraconfig('biblioserver')->{config};
1149     print {$zd} "
1150 # generated by KOHA/misc/migrtion_tools/rebuild_zebra.pl
1151 profilePath:\${srcdir:-.}:$biblioserverdir/tab/:$tabdir/tab/:\${srcdir:-.}/tab/
1152
1153 encoding: UTF-8
1154 # Files that describe the attribute sets supported.
1155 attset:bib1.att
1156 attset:explain.att
1157 attset:gils.att
1158
1159 modulePath:$modulesdir/modules/
1160 # Specify record type
1161 iso2709.recordType:grs.marcxml.record
1162 recordType:grs.xml
1163 recordId: (bib1,Local-Number)
1164 storeKeys:1
1165 storeData:1
1166
1167
1168 # Lock File Area
1169 lockDir: $biblioserverdir/lock
1170 perm.anonymous:r
1171 perm.kohaadmin:rw
1172 register: $biblioserverdir/register:4G
1173 shadow: $biblioserverdir/shadow:4G
1174
1175 # Temp File area for result sets
1176 setTmpDir: $biblioserverdir/tmp
1177
1178 # Temp File area for index program
1179 keyTmpDir: $biblioserverdir/key
1180
1181 # Approx. Memory usage during indexing
1182 memMax: 40M
1183 rank:rank-1
1184     ";
1185         print "Info: creating zebra-biblios.cfg\n";
1186         $created_dir_or_file++;
1187     }
1188
1189     if ($created_dir_or_file) {
1190         print "Info: created : $created_dir_or_file directories & files\n";
1191     } else {
1192         print "Info: file & directories OK\n";
1193     }
1194
1195 }
1196 }