root/gsdl/trunk/perllib/basebuildproc.pm @ 18456

Revision 18456, 20.4 KB (checked in by davidb, 11 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
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 browser.