source: gsdl/trunk/perllib/basebuildproc.pm@ 18456

Last change on this file since 18456 was 18456, checked in by davidb, 15 years ago

Additions to support the deleting of documents from the index. Only works for indexers that support incremental building, e.g. lucene

  • Property svn:keywords set to Author Date Id Revision
File size: 20.4 KB
Line 
1###########################################################################
2#
3# basebuildproc.pm --
4# A component of the Greenstone digital library software
5# from the New Zealand Digital Library Project at the
6# University of Waikato, New Zealand.
7#
8# Copyright (C) 1999 New Zealand Digital Library Project
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation; either version 2 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program; if not, write to the Free Software
22# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
23#
24###########################################################################
25
26# This document processor outputs a document for indexing (should be
27# implemented by subclass) and storing in the database
28
29package basebuildproc;
30
31eval {require bytes};
32
33use classify;
34use dbutil;
35use doc;
36use docproc;
37use strict;
38no strict 'subs';
39no strict 'refs';
40use util;
41
42BEGIN {
43 @basebuildproc::ISA = ('docproc');
44}
45
46sub new()
47 {
48 my ($class, $collection, $source_dir, $build_dir, $keepold, $verbosity, $outhandle) = @_;
49 my $self = new docproc ();
50
51 # outhandle is where all the debugging info goes
52 # output_handle is where the output of the plugins is piped
53 # to (i.e. mg, database etc.)
54 $outhandle = STDERR unless defined $outhandle;
55
56 $self->{'collection'} = $collection;
57 $self->{'source_dir'} = $source_dir;
58 $self->{'build_dir'} = $build_dir;
59 $self->{'keepold'} = $keepold;
60 $self->{'verbosity'} = $verbosity;
61 $self->{'outhandle'} = $outhandle;
62
63 $self->{'classifiers'} = [];
64 $self->{'mode'} = "text";
65 $self->{'assocdir'} = $build_dir;
66 $self->{'dontdb'} = {};
67 $self->{'store_metadata_coverage'} = "false";
68
69 $self->{'index'} = "section:text";
70 $self->{'indexexparr'} = [];
71
72 $self->{'separate_cjk'} = 0;
73
74 my $found_num_data = 0;
75 my $buildconfigfile = undef;
76
77 if ($keepold) {
78 # For incremental building need to seed num_docs etc from values
79 # stored in build.cfg (if present)
80 $buildconfigfile = &util::filename_cat($build_dir, "build.cfg");
81 if (-e $buildconfigfile) {
82 $found_num_data = 1;
83 }
84 else {
85 # try the index dir
86 $buildconfigfile = &util::filename_cat($ENV{'GSDLCOLLECTDIR'},
87 "index", "build.cfg");
88 if (-e $buildconfigfile) {
89 $found_num_data = 1;
90 }
91 }
92
93 }
94
95 if ($found_num_data)
96 {
97 #print STDERR "Found_Num_Data!\n";
98 my $buildcfg = &colcfg::read_build_cfg($buildconfigfile);
99 $self->{'starting_num_docs'} = $buildcfg->{'numdocs'};
100 #print STDERR "- num_docs: $self->{'starting_num_docs'}\n";
101 $self->{'starting_num_sections'} = $buildcfg->{'numsections'};
102 #print STDERR "- num_sections: $self->{'starting_num_sections'}\n";
103 $self->{'starting_num_bytes'} = $buildcfg->{'numbytes'};
104 #print STDERR "- num_bytes: $self->{'starting_num_bytes'}\n";
105 }
106 else
107 {
108 #print STDERR "NOT Found_Num_Data!\n";
109 $self->{'starting_num_docs'} = 0;
110 $self->{'starting_num_sections'} = 0;
111 $self->{'starting_num_bytes'} = 0;
112 }
113
114 $self->{'output_handle'} = "STDOUT";
115 $self->{'num_docs'} = $self->{'starting_num_docs'};
116 $self->{'num_sections'} = $self->{'starting_num_sections'};
117 $self->{'num_bytes'} = $self->{'starting_num_bytes'};
118
119 $self->{'num_processed_bytes'} = 0;
120 $self->{'store_text'} = 1;
121
122 # what level (section/document) the database - indexer intersection is
123 $self->{'db_level'} = "section";
124 #used by browse interface
125 $self->{'doclist'} = [];
126
127 $self->{'indexing_text'} = 0;
128
129 return bless $self, $class;
130
131}
132
133sub reset {
134 my $self = shift (@_);
135
136 $self->{'num_docs'} = $self->{'starting_num_docs'};
137 $self->{'num_sections'} = $self->{'starting_num_sections'};
138 $self->{'num_bytes'} = $self->{'starting_num_bytes'};
139
140 $self->{'num_processed_bytes'} = 0;
141}
142
143sub zero_reset {
144 my $self = shift (@_);
145
146 $self->{'num_docs'} = 0;
147 $self->{'num_sections'} = 0;
148 # reconstructed docs have no text, just metadata, so we need to
149 # remember how many bytes we had initially
150 $self->{'num_bytes'} = $self->{'starting_num_bytes'};
151
152 $self->{'num_processed_bytes'} = 0;
153}
154
155sub is_incremental_capable
156{
157 # By default we return 'no' as the answer
158 # Safer to assume non-incremental to start with, and then override in
159 # inherited classes that are.
160
161 return 0;
162}
163
164sub get_num_docs {
165 my $self = shift (@_);
166
167 return $self->{'num_docs'};
168}
169
170sub get_num_sections {
171 my $self = shift (@_);
172
173 return $self->{'num_sections'};
174}
175
176# num_bytes is the actual number of bytes in the collection
177# this is normally the same as what's processed during text compression
178sub get_num_bytes {
179 my $self = shift (@_);
180
181 return $self->{'num_bytes'};
182}
183
184# num_processed_bytes is the number of bytes actually passed
185# to mg for the current index
186sub get_num_processed_bytes {
187 my $self = shift (@_);
188
189 return $self->{'num_processed_bytes'};
190}
191
192sub set_output_handle {
193 my $self = shift (@_);
194 my ($handle) = @_;
195
196 $self->{'output_handle'} = $handle;
197}
198
199
200sub set_mode {
201 my $self = shift (@_);
202 my ($mode) = @_;
203
204 $self->{'mode'} = $mode;
205}
206
207sub get_mode {
208 my $self = shift (@_);
209
210 return $self->{'mode'};
211}
212
213sub set_assocdir {
214 my $self = shift (@_);
215 my ($assocdir) = @_;
216
217 $self->{'assocdir'} = $assocdir;
218}
219
220sub set_dontdb {
221 my $self = shift (@_);
222 my ($dontdb) = @_;
223
224 $self->{'dontdb'} = $dontdb;
225}
226
227sub set_infodbtype
228{
229 my $self = shift(@_);
230 my $infodbtype = shift(@_);
231 $self->{'infodbtype'} = $infodbtype;
232}
233
234sub set_index {
235 my $self = shift (@_);
236 my ($index, $indexexparr) = @_;
237
238 $self->{'index'} = $index;
239 $self->{'indexexparr'} = $indexexparr if defined $indexexparr;
240}
241
242sub set_index_languages {
243 my $self = shift (@_);
244 my ($lang_meta, $langarr) = @_;
245 $self->{'lang_meta'} = $lang_meta;
246 $self->{'langarr'} = $langarr;
247}
248
249sub get_index {
250 my $self = shift (@_);
251
252 return $self->{'index'};
253}
254
255sub set_classifiers {
256 my $self = shift (@_);
257 my ($classifiers) = @_;
258
259 $self->{'classifiers'} = $classifiers;
260}
261
262sub set_indexing_text {
263 my $self = shift (@_);
264 my ($indexing_text) = @_;
265
266 $self->{'indexing_text'} = $indexing_text;
267}
268
269sub get_indexing_text {
270 my $self = shift (@_);
271
272 return $self->{'indexing_text'};
273}
274
275sub set_store_text {
276 my $self = shift (@_);
277 my ($store_text) = @_;
278
279 $self->{'store_text'} = $store_text;
280}
281
282sub set_store_metadata_coverage {
283 my $self = shift (@_);
284 my ($store_metadata_coverage) = @_;
285
286 $self->{'store_metadata_coverage'} = $store_metadata_coverage || "";
287}
288
289sub get_doc_list {
290 my $self = shift(@_);
291
292 return @{$self->{'doclist'}};
293}
294
295# the standard database level is section, but you may want to change it to document
296sub set_db_level {
297 my $self= shift (@_);
298 my ($db_level) = @_;
299
300 $self->{'db_level'} = $db_level;
301}
302
303sub set_sections_index_document_metadata {
304 my $self= shift (@_);
305 my ($index_type) = @_;
306
307 $self->{'sections_index_document_metadata'} = $index_type;
308}
309
310sub set_separate_cjk {
311 my $self = shift (@_);
312 my ($sep_cjk) = @_;
313
314 $self->{'separate_cjk'} = $sep_cjk;
315}
316
317sub process {
318 my $self = shift (@_);
319 my $method = $self->{'mode'};
320
321 $self->$method(@_);
322}
323
324# post process text depending on field. Currently don't do anything here
325# except cjk separation, and only for indexing
326# should only do this for indexed text (if $self->{'indexing_text'}),
327# but currently search term highlighting doesn't work if you do that.
328# once thats fixed up, then fix this.
329sub filter_text {
330 my $self = shift (@_);
331 my ($field, $text) = @_;
332
333 # lets do cjk seg here
334 my $new_text =$text;
335 if ($self->{'separate_cjk'}) {
336 $new_text = &cnseg::segment($text);
337 }
338 return $new_text;
339}
340
341
342sub infodb_metadata_stats
343{
344 my $self = shift (@_);
345 my ($field) = @_;
346
347 # Keep some statistics relating to metadata sets used and
348 # frequency of particular metadata fields within each set
349
350 # Union of metadata prefixes and frequency of fields
351 # (both scoped for this document alone, and across whole collection)
352
353 if ($field =~ m/^(.+)\.(.*)$/) {
354 my $prefix = $1;
355 my $core_field = $2;
356
357 $self->{'doc_mdprefix_fields'}->{$prefix}->{$core_field}++;
358 $self->{'mdprefix_fields'}->{$prefix}->{$core_field}++;
359 }
360 elsif ($field =~ m/^[[:upper:]]/) {
361 # implicit 'ex' metadata set
362
363 $self->{'doc_mdprefix_fields'}->{'ex'}->{$field}++;
364 $self->{'mdprefix_fields'}->{'ex'}->{$field}++;
365 }
366
367}
368
369
370sub infodbedit {
371 my $self = shift (@_);
372 my ($doc_obj, $filename, $edit_mode) = @_;
373
374 # only output this document if it is a "indexed_doc" or "info_doc" (database only) document
375 my $doctype = $doc_obj->get_doc_type();
376 return if ($doctype ne "indexed_doc" && $doctype ne "info_doc");
377
378 my $archivedir = "";
379 if (defined $filename)
380 {
381 # doc_obj derived directly from file
382 my ($dir) = $filename =~ /^(.*?)(?:\/|\\)[^\/\\]*$/;
383 $dir = "" unless defined $dir;
384 $dir =~ s/\\/\//g;
385 $dir =~ s/^\/+//;
386 $dir =~ s/\/+$//;
387
388 $archivedir = $dir;
389
390 # resolve the final filenames of the files associated with this document
391 $self->assoc_files ($doc_obj, $archivedir);
392 }
393 else
394 {
395 # doc_obj reconstructed from database (has metadata, doc structure but no text)
396 my $top_section = $doc_obj->get_top_section();
397 $archivedir = $doc_obj->get_metadata_element($top_section,"archivedir");
398 }
399
400 if (($edit_mode eq "add") || ($edit_mode eq "reindex")) {
401 #add this document to the browse structure
402 push(@{$self->{'doclist'}},$doc_obj->get_OID())
403 unless ($doctype eq "classification");
404 }
405 else {
406 # delete => remove this doc from browse structure
407 my $del_doc_oid = $doc_obj->get_OID();
408
409 my @filtered_doc_list = ();
410 foreach my $oid (@{$self->{'doclist'}}) {
411 push(@filtered_doc_list,$oid) if ($oid ne $del_doc_oid);
412 }
413 $self->{'doclist'} = \@filtered_doc_list;
414 }
415
416
417 # classify this document
418 &classify::classify_doc ($self->{'classifiers'}, $doc_obj, $edit_mode);
419
420 if (($edit_mode eq "add") || ($edit_mode eq "reindex")) {
421 # this is another document
422 $self->{'num_docs'} += 1 unless ($doctype eq "classification");
423 }
424 else {
425 # delete
426 $self->{'num_docs'} -= 1 unless ($doctype eq "classification");
427 return;
428 }
429
430 # is this a paged or a hierarchical document
431 my ($thistype, $childtype) = $self->get_document_type ($doc_obj);
432
433 my $section = $doc_obj->get_top_section ();
434 my $doc_OID = $doc_obj->get_OID();
435 my $first = 1;
436 my $infodb_handle = $self->{'output_handle'};
437
438 $self->{'doc_mdprefix_fields'} = {};
439
440 while (defined $section)
441 {
442 my $section_OID = $doc_OID;
443 if ($section ne "")
444 {
445 $section_OID = $doc_OID . "." . $section;
446 }
447 my %section_infodb = ();
448
449 # update a few statistics
450 $self->{'num_bytes'} += $doc_obj->get_text_length ($section);
451 $self->{'num_sections'} += 1 unless ($doctype eq "classification");
452
453 # output the fact that this document is a document (unless doctype
454 # has been set to something else from within a plugin
455 my $dtype = $doc_obj->get_metadata_element ($section, "doctype");
456 if (!defined $dtype || $dtype !~ /\w/) {
457 $section_infodb{"doctype"} = [ "doc" ];
458 }
459
460 # Output whether this node contains text
461 #
462 # If doc_obj reconstructed from database file then no need to
463 # explicitly add <hastxt> as this is preserved as metadata when
464 # the database file is loaded in
465 if (defined $filename)
466 {
467 # doc_obj derived directly from file
468 if ($doc_obj->get_text_length($section) > 0) {
469 $section_infodb{"hastxt"} = [ "1" ];
470 } else {
471 $section_infodb{"hastxt"} = [ "0" ];
472 }
473 }
474
475 # output all the section metadata
476 my $metadata = $doc_obj->get_all_metadata ($section);
477 foreach my $pair (@$metadata) {
478 my ($field, $value) = (@$pair);
479
480 if ($field ne "Identifier" && $field !~ /^gsdl/ &&
481 defined $value && $value ne "") {
482
483 # escape problematic stuff
484 $value =~ s/\\/\\\\/g;
485 $value =~ s/\n/\\n/g;
486 $value =~ s/\r/\\r/g;
487
488 # special case for URL metadata
489 if ($field =~ /^URL$/i) {
490 &dbutil::write_infodb_entry($self->{'infodbtype'}, $infodb_handle, $value, { 'section' => [ $section_OID ] });
491 }
492
493 if (!defined $self->{'dontdb'}->{$field}) {
494 push(@{$section_infodb{$field}}, $value);
495
496 if ($section eq "" && $self->{'store_metadata_coverage'} =~ /^true$/i)
497 {
498 $self->infodb_metadata_stats($field);
499 }
500 }
501 }
502 }
503
504 if ($section eq "")
505 {
506 my $doc_mdprefix_fields = $self->{'doc_mdprefix_fields'};
507
508 foreach my $prefix (keys %$doc_mdprefix_fields)
509 {
510 push(@{$section_infodb{"metadataset"}}, $prefix);
511
512 foreach my $field (keys %{$doc_mdprefix_fields->{$prefix}})
513 {
514 push(@{$section_infodb{"metadatalist-$prefix"}}, $field);
515
516 my $val = $doc_mdprefix_fields->{$prefix}->{$field};
517 push(@{$section_infodb{"metadatafreq-$prefix-$field"}}, $val);
518 }
519 }
520 }
521
522 # If doc_obj reconstructed from database file then no need to
523 # explicitly add <archivedir> as this is preserved as metadata when
524 # the database file is loaded in
525 if (defined $filename)
526 {
527 # output archivedir if at top level
528 if ($section eq $doc_obj->get_top_section()) {
529 $section_infodb{"archivedir"} = [ $archivedir ];
530 }
531 }
532
533 # output document display type
534 if ($first) {
535 $section_infodb{"thistype"} = [ $thistype ];
536 }
537
538 if ($self->{'db_level'} eq "document") {
539 # doc num is num_docs not num_sections
540 # output the matching document number
541 $section_infodb{"docnum"} = [ $self->{'num_docs'} ];
542 }
543 else {
544 # output a list of children
545 my $children = $doc_obj->get_children ($section);
546 if (scalar(@$children) > 0) {
547 $section_infodb{"childtype"} = [ $childtype ];
548 my $contains = "";
549 foreach my $child (@$children)
550 {
551 $contains .= ";" unless ($contains eq "");
552 if ($child =~ /^.*?\.(\d+)$/)
553 {
554 $contains .= "\".$1";
555 }
556 else
557 {
558 $contains .= "\".$child";
559 }
560 }
561 $section_infodb{"contains"} = [ $contains ];
562 }
563 # output the matching doc number
564 $section_infodb{"docnum"} = [ $self->{'num_sections'} ];
565 }
566
567 &dbutil::write_infodb_entry($self->{'infodbtype'}, $infodb_handle, $section_OID, \%section_infodb);
568
569 # output a database entry for the document number, except for Lucene (which no longer needs this information)
570 unless (ref($self) eq "lucenebuildproc")
571 {
572 if ($self->{'db_level'} eq "document") {
573 &dbutil::write_infodb_entry($self->{'infodbtype'}, $infodb_handle, $self->{'num_docs'}, { 'section' => [ $doc_OID ] });
574 }
575 else {
576 &dbutil::write_infodb_entry($self->{'infodbtype'}, $infodb_handle, $self->{'num_sections'}, { 'section' => [ $section_OID ] });
577 }
578 }
579
580 $first = 0;
581 $section = $doc_obj->get_next_section($section);
582 last if ($self->{'db_level'} eq "document"); # if no sections wanted, only add the docs
583 }
584}
585
586
587
588
589sub infodb {
590 my $self = shift (@_);
591 my ($doc_obj, $filename) = @_;
592
593 $self->infodbedit($doc_obj,$filename,"add");
594}
595
596sub infodbreindex {
597 my $self = shift (@_);
598 my ($doc_obj, $filename) = @_;
599
600 $self->infodbedit($doc_obj,$filename,"reindex");
601}
602
603sub infodbdelete {
604 my $self = shift (@_);
605 my ($doc_obj, $filename) = @_;
606
607 $self->infodbedit($doc_obj,$filename,"delete");
608}
609
610
611sub text {
612 my $self = shift (@_);
613 my ($doc_obj) = @_;
614
615 my $handle = $self->{'outhandle'};
616 print $handle "basebuildproc::text function must be implemented in sub classes\n";
617 die "\n";
618}
619
620sub textreindex
621{
622 my $self = shift @_;
623
624 my $outhandle = $self->{'outhandle'};
625 print $outhandle "basebuildproc::textreindex function must be implemented in sub classes\n";
626 if (!$self->is_incremental_capable()) {
627
628 print $outhandle " This operation is only possible with indexing tools with that support\n";
629 print $outhandle " incremental building\n";
630 }
631 die "\n";
632}
633
634sub textdelete
635{
636 my $self = shift @_;
637
638 my $outhandle = $self->{'outhandle'};
639 print $outhandle "basebuildproc::textdelete function must be implemented in sub classes\n";
640 if (!$self->is_incremental_capable()) {
641
642 print $outhandle " This operation is only possible with indexing tools with that support\n";
643 print $outhandle " incremental building\n";
644 }
645 die "\n";
646}
647
648
649# should the document be indexed - according to the subcollection and language
650# specification.
651sub is_subcollection_doc {
652 my $self = shift (@_);
653 my ($doc_obj) = @_;
654
655 my $indexed_doc = 1;
656 foreach my $indexexp (@{$self->{'indexexparr'}}) {
657 $indexed_doc = 0;
658 my ($field, $exp, $options) = split /\//, $indexexp;
659 if (defined ($field) && defined ($exp)) {
660 my ($bool) = $field =~ /^(.)/;
661 $field =~ s/^.// if $bool eq '!';
662 my @metadata_values;
663 if ($field =~ /^filename$/i) {
664 push(@metadata_values, $doc_obj->get_source_filename());
665 }
666 else {
667 @metadata_values = @{$doc_obj->get_metadata($doc_obj->get_top_section(), $field)};
668 }
669 next unless @metadata_values;
670 foreach my $metadata_value (@metadata_values) {
671 if ($bool eq '!') {
672 if ($options =~ /^i$/i) {
673 if ($metadata_value !~ /$exp/i) {$indexed_doc = 1; last;}
674 } else {
675 if ($metadata_value !~ /$exp/) {$indexed_doc = 1; last;}
676 }
677 } else {
678 if ($options =~ /^i$/i) {
679 if ($metadata_value =~ /$exp/i) {$indexed_doc = 1; last;}
680 } else {
681 if ($metadata_value =~ /$exp/) {$indexed_doc = 1; last;}
682 }
683 }
684 }
685
686 last if ($indexed_doc == 1);
687 }
688 }
689
690 # if this doc is so far in the sub collection, and we have lang info,
691 # now we check the languages to see if it matches
692 if($indexed_doc && defined $self->{'lang_meta'}) {
693 $indexed_doc = 0;
694 my $field = $doc_obj->get_metadata_element($doc_obj->get_top_section(), $self->{'lang_meta'});
695 if (defined $field) {
696 foreach my $lang (@{$self->{'langarr'}}) {
697 my ($bool) = $lang =~ /^(.)/;
698 if ($bool eq '!') {
699 $lang =~ s/^.//;
700 if ($field !~ /$lang/) {
701 $indexed_doc = 1; last;
702 }
703 } else {
704 if ($field =~ /$lang/) {
705 $indexed_doc = 1; last;
706 }
707 }
708 }
709 }
710 }
711 return $indexed_doc;
712
713}
714
715# use 'Paged' if document has no more than 2 levels
716# and each section at second level has a number for
717# Title metadata
718# also use Paged if gsdlthistype metadata is set to Paged
719sub get_document_type {
720 my $self = shift (@_);
721 my ($doc_obj) = @_;
722
723 my $thistype = "VList";
724 my $childtype = "VList";
725 my $title;
726 my @tmp = ();
727
728 my $section = $doc_obj->get_top_section ();
729
730 my $gsdlthistype = $doc_obj->get_metadata_element ($section, "gsdlthistype");
731 if (defined $gsdlthistype) {
732 if ($gsdlthistype eq "Paged") {
733 $childtype = "Paged";
734 if ($doc_obj->get_text_length ($doc_obj->get_top_section())) {
735 $thistype = "Paged";
736 } else {
737 $thistype = "Invisible";
738 }
739
740 return ($thistype, $childtype);
741 } elsif ($gsdlthistype eq "Hierarchy") {
742 return ($thistype, $childtype); # use VList, VList
743 }
744 }
745 my $first = 1;
746 while (defined $section) {
747 @tmp = split /\./, $section;
748 if (scalar(@tmp) > 1) {
749 return ($thistype, $childtype);
750 }
751 if (!$first) {
752 $title = $doc_obj->get_metadata_element ($section, "Title");
753 if (!defined $title || $title !~ /^\d+$/) {
754 return ($thistype, $childtype);
755 }
756 }
757 $first = 0;
758 $section = $doc_obj->get_next_section($section);
759 }
760 if ($doc_obj->get_text_length ($doc_obj->get_top_section())) {
761 $thistype = "Paged";
762 } else {
763 $thistype = "Invisible";
764 }
765 $childtype = "Paged";
766 return ($thistype, $childtype);
767}
768
769sub assoc_files
770{
771 my $self = shift (@_);
772 my ($doc_obj, $archivedir) = @_;
773 my ($afile);
774
775 foreach my $assoc_file (@{$doc_obj->get_assoc_files()}) {
776 #rint STDERR "Processing associated file - copy " . $assoc_file->[0] . " to " . $assoc_file->[1] . "\n";
777 # if assoc file starts with a slash, we put it relative to the assoc
778 # dir, otherwise it is relative to the HASH... directory
779 if ($assoc_file->[1] =~ m@^[/\\]@) {
780 $afile = &util::filename_cat($self->{'assocdir'}, $assoc_file->[1]);
781 } else {
782 $afile = &util::filename_cat($self->{'assocdir'}, $archivedir, $assoc_file->[1]);
783 }
784 &util::hard_link ($assoc_file->[0], $afile);
785 }
786}
787
Note: See TracBrowser for help on using the repository browser.