source: trunk/gsdl/perllib/docsave.pm@ 9954

Last change on this file since 9954 was 9954, checked in by davidb, 19 years ago

The abilities to save documents from import.pl and export.pl are very similar,
but the code did not always reflect this. At times it was repeatative and
at others inconsistent. This has now been refactored and brought in to
line.

  • Property svn:keywords set to Author Date Id Revision
File size: 19.5 KB
Line 
1###########################################################################
2#
3# docsave.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 saves a document in the
27# archives directory of a collection (as xml)
28
29use strict;
30no strict 'refs';
31
32package docsave;
33
34eval {require bytes};
35
36use arcinfo;
37use expinfo;
38use docproc;
39use util;
40
41
42sub BEGIN {
43 @docsave::ISA = ('docproc');
44}
45
46sub new {
47 my ($class, $collection, $info, $verbosity,
48 $gzip, $groupsize, $outhandle, $service, $saveas) = @_;
49 my $self = new docproc ();
50
51 my $collectdir = $ENV{'GSDLCOLLECTDIR'};
52
53 $outhandle = 'STDERR' unless (defined $outhandle);
54 $service = "import" unless (defined $service);
55 $saveas = "GA" unless (defined $saveas);
56 $groupsize = 1 unless (defined $groupsize);
57
58 $self->{'collection'} = $collection;
59 if (($service eq "import") || ($service eq "unbuild")) {
60 $self->{'archive_info'} = $info;
61 # set a default for the archive directory
62 $self->{'archive_dir'} = &util::filename_cat ($collectdir, "archives");
63 } elsif ($service eq "export"){
64 $self->{'export_info'} = $info;
65 # set a default for the export directory
66 $self->{'export_dir'} = &util::filename_cat($collectdir, "export");
67 } else {
68 print $outhandle "docsave::new Unrecongised service: $service\n";
69 return;
70 }
71
72 $self->{'verbosity'} = $verbosity;
73 $self->{'gzip'} = $gzip;
74 $self->{'keepimportstructure'} = 0;
75 $self->{'groupsize'} = $groupsize;
76 $self->{'gs_count'} = 0;
77
78 $self->{'outhandle'} = $outhandle;
79 $self->{'service'} = $service;
80 $self->{'saveas'} = $saveas;
81
82 $self->{'sortmeta'} = undef;
83
84 return bless $self, $class;
85}
86
87sub setarchivedir {
88 my $self = shift (@_);
89 my ($archive_dir) = @_;
90
91 &util::mk_all_dir ($archive_dir) unless -e $archive_dir;
92 $self->{'archive_dir'} = $archive_dir;
93}
94
95sub setexportdir {
96 my $self = shift (@_);
97 my ($export_dir) = @_;
98
99 &util::mk_all_dir ($export_dir) unless -e $export_dir;
100 $self->{'export_dir'} = $export_dir;
101}
102
103sub getoutputdir {
104 my $self = shift (@_);
105
106 my $output_dir = undef;
107
108 my $service = $self->{'service'};
109
110 if (($service eq "import") || ($service eq "unbuild")) {
111 $output_dir = $self->{'archive_dir'};
112 }
113 elsif ($service eq "export") {
114 $output_dir = $self->{'export_dir'};
115 }
116 else {
117 my $outhandle = $self->{'outhandle'};
118
119 print $outhandle "docsave::getoutputdir did not recognise service ";
120 print $outhandle " '$service'. No output directory set.\n";
121 }
122
123 return $output_dir;
124}
125
126
127sub getoutputinfo {
128 my $self = shift (@_);
129
130 my $output_info = undef;
131
132 my $service = $self->{'service'};
133
134 if (($service eq "import") || ($service eq "unbuild")) {
135 $output_info = $self->{'archive_info'};
136 }
137 elsif ($service eq "export") {
138 $output_info = $self->{'export_info'};
139 }
140 else {
141 my $outhandle = $self->{'outhandle'};
142
143 print $outhandle "docsave::getoutputinfo did not recognise service ";
144 print $outhandle " '$service'. No output information available.\n";
145 }
146
147 return $output_info;
148}
149
150
151sub set_sortmeta {
152 my $self = shift (@_);
153 my ($sortmeta, $removeprefix, $removesuffix) = @_;
154
155 $self->{'sortmeta'} = $sortmeta;
156 if (defined ($removeprefix) && $removeprefix ) {
157 $removeprefix =~ s/^\^//; # don't need a leading ^
158 $self->{'removeprefix'} = $removeprefix;
159 }
160 if (defined ($removesuffix) && $removesuffix) {
161 $removesuffix =~ s/\$$//; # don't need a trailing $
162 $self->{'removesuffix'} = $removesuffix;
163 }
164}
165
166sub process {
167 my $self = shift (@_);
168 my ($doc_obj) = @_;
169
170 my $outhandle = $self->{'outhandle'};
171 my $service = $self->{'service'};
172
173 # Define the SaveAs Type
174 my $save_as = $self->{'saveas'};
175 my $collection = $self->{'collection'};
176
177 if ($self->{'groupsize'} > 1) {
178 $self->group_process ($doc_obj);
179 return;
180 }
181
182 my $OID = $doc_obj->get_OID();
183 $OID = "NULL" unless defined $OID;
184
185 my $top_section = $doc_obj->get_top_section();
186
187 # get document's directory
188 my $doc_dir = $self->get_doc_dir ($OID, $doc_obj->get_source_filename());
189
190 # groupsize is 1 (i.e. one document per XML file) so sortmeta
191 # may be used
192
193 my $output_info = $self->getoutputinfo();
194 return if (!defined $output_info);
195
196 my $output_dir = $self->getoutputdir();
197 my $working_dir = &util::filename_cat ($output_dir, $doc_dir);
198
199 # copy all the associated files, add this information as metadata
200 # to the document
201 if ($save_as eq "DSpace") {
202
203 # Genereate handle file
204 # (Note: this section of code would benefit from being restructured)
205 my $doc_handle_file = &util::filename_cat ($working_dir, "handle");
206
207 my $env_hp = $ENV{'DSPACE_HANDLE_PREFIX'};
208 my $handle_prefix = (defined $env_hp) ? $env_hp : "123456789";
209
210 if (!open(OUTDOC_HANDLE,">$doc_handle_file")){
211 print $outhandle "docsave::process could not write collection handle to file $doc_handle_file\n";
212 return;
213 }
214
215 my ($handle) = ($doc_dir =~ m/^(.*)\.dir$/);
216 print OUTDOC_HANDLE "$handle_prefix/$handle\n";
217
218 close OUTDOC_HANDLE;
219
220 # Generate contents file
221 my $doc_contents_file = &util::filename_cat ($working_dir, "contents");
222
223 if (!open(OUTDOC_CONTENTS,">$doc_contents_file")){
224 print $outhandle "docsave::process could not write collection contents to file $doc_contents_file\n";
225 return;
226 }
227 $self->process_assoc_files ($doc_obj, $doc_dir, 'docsave::OUTDOC_CONTENTS');
228
229 close OUTDOC_CONTENTS;
230
231 } else {
232 $self->process_assoc_files ($doc_obj, $doc_dir, '');
233 }
234
235 # Save the document in the requested 'save_as' format
236
237 if ($save_as eq "GA") {
238
239 my $doc_file = &util::filename_cat ($working_dir, "doc.xml");
240
241 if (!open (OUTDOC, ">$doc_file")) {
242 print $outhandle "docsave::process could not write to file $doc_file\n";
243 return;
244 }
245
246 # save this document
247 $self->output_xml_header('docsave::OUTDOC');
248 $doc_obj->output_section('docsave::OUTDOC',$top_section);
249 $self->output_xml_footer('docsave::OUTDOC');
250
251 close OUTDOC;
252 }
253 elsif ($save_as eq "METS") {
254
255 my $doc_txt_file = &util::filename_cat ($working_dir,"doctxt.xml");
256
257 if (!open(OUTDOC_TXT, ">$doc_txt_file")){
258 print $outhandle "docsave::process could not write to file $doc_txt_file\n";
259 return;
260 }
261
262 $self->output_txt_xml_header('docsave::OUTDOC_TXT');
263 $doc_obj->output_txt_section('docsave::OUTDOC_TXT', $top_section);
264 $self->output_txt_xml_footer('docsave::OUTDOC_TXT');
265
266 close OUTDOC_TXT;
267
268 # Now save the document with metadata and text structure to docmets.xml
269
270 my $doc_mets_file = &util::filename_cat ($working_dir, "docmets.xml");
271
272 my $doc_title = $doc_obj->get_metadata_element($top_section,"dc.Title");
273 if (!defined $doc_title) {
274 $doc_title = $doc_obj->get_metadata_element($top_section,"Title");
275 }
276
277 if (!open(OUTDOC_METS,">$doc_mets_file")){
278 print $outhandle "docsave::process could not write to file $doc_mets_file\n";
279 return;
280 }
281
282 my $saveas_version = $self->{'saveas_version'};
283 $self->output_mets_xml_header('docsave::OUTDOC_METS', $OID, $doc_title);
284 $doc_obj->output_mets_section('docsave::OUTDOC_METS',$top_section,$saveas_version,$working_dir);
285 $self->output_mets_xml_footer('docsave::OUTDOC_METS');
286
287 close OUTDOC_METS;
288 }
289 elsif ($save_as eq "DSpace") {
290
291 # Generate dublin_core.xml file
292 my $doc_dc_file = &util::filename_cat ($working_dir, "dublin_core.xml");
293
294 if (!open(OUTDOC_DC,">$doc_dc_file")){
295 print $outhandle "docsave::process could not write dublin core to file $doc_dc_file\n";
296 return;
297 }
298
299 my $saveas_version = $self->{'saveas_version'};
300
301 $self->output_dc_xml_header('docsave::OUTDOC_DC', $OID);
302 $doc_obj->output_dc_section('docsave::OUTDOC_DC',$top_section);
303 $self->output_dc_xml_footer('docsave::OUTDOC_DC');
304
305 close OUTDOC_DC;
306 } else { # save_as isn't one of the recognised types
307 print $outhandle "docsave::process unrecognised saveas type, $save_as\n";
308 return;
309 }
310
311
312 my $short_doc_file;
313
314 if ($save_as eq "GA") {
315 $short_doc_file = util::filename_cat ($doc_dir, "doc.xml");
316 } elsif ($save_as eq "METS") {
317 $short_doc_file = &util::filename_cat ($doc_dir, "docmets.xml");
318 } elsif ($save_as eq "DSpace") {
319 $short_doc_file=&util::filename_cat ($doc_dir, "dublin_core.xml");
320 } else {
321 return;
322 }
323
324 #save for later (for close_file_output())
325 $self->{'short_doc_file'} = $short_doc_file;
326
327 if ($self->{'gzip'}) {
328 my $doc_file = $self->{'gs_filename'};
329 `gzip $doc_file`;
330 $doc_file .= ".gz";
331 $short_doc_file .= ".gz";
332 if (!-e $doc_file) {
333 print $outhandle "error while gzipping: $doc_file doesn't exist\n";
334 return 0;
335 }
336 }
337
338 # do the sortmeta thing
339 my ($metadata);
340 if (defined ($self->{'sortmeta'})) {
341 $metadata = $doc_obj->get_metadata_element($top_section,$self->{'sortmeta'});
342 }
343 if (defined ($metadata) && $metadata) {
344 # do remove prefix/suffix
345 if (defined($self->{'removeprefix'})) {
346 $metadata =~ s/^$self->{'removeprefix'}//;
347 }
348 if (defined($self->{'removesuffix'})) {
349 $metadata =~ s/$self->{'removesuffix'}$//;
350 }
351 $metadata = &sorttools::format_metadata_for_sorting($self->{'sortmeta'}, $metadata, $doc_obj);
352 }
353
354 # store reference in the relevant info object (archive_info,export_info,...)
355 $output_info->add_info($OID, $short_doc_file, $metadata);
356}
357
358
359sub group_process {
360 my $self = shift (@_);
361 my ($doc_obj) = @_;
362
363 my $outhandle = $self->{'outhandle'};
364
365 my $OID = $doc_obj->get_OID();
366 $OID = "NULL" unless defined $OID;
367
368 my $groupsize = $self->{'groupsize'};
369 my $gs_count = $self->{'gs_count'};
370 my $open_new_file = (($gs_count % $groupsize)==0);
371
372 # opening a new file, or document has assoicated files => directory needed
373 if (($open_new_file) || (scalar(@{$doc_obj->get_assoc_files()})>0)) {
374
375 # get document's directory
376 my $doc_dir = $self->get_doc_dir ($OID, $doc_obj->get_source_filename());
377
378 # copy all the associated files, add this information as metadata
379 # to the document
380 $self->process_assoc_files ($doc_obj, $doc_dir);
381
382
383 if ($open_new_file) {
384 # only if opening new file
385 my $doc_file
386 = &util::filename_cat ($self->{'archive_dir'}, $doc_dir, "doc.xml");
387 my $short_doc_file = &util::filename_cat ($doc_dir, "doc.xml");
388
389 if ($gs_count>0)
390 {
391 return if (!$self->close_file_output());
392 }
393
394 if (!open (OUTDOC, ">$doc_file")) {
395 print $outhandle "docsave::group_process could not write to file $doc_file\n";
396 return;
397 }
398 $self->{'gs_filename'} = $doc_file;
399 $self->{'gs_short_filename'} = $short_doc_file;
400 $self->{'gs_OID'} = $OID;
401
402 $self->output_xml_header('docsave::OUTDOC');
403 }
404 }
405
406 # save this document
407 $doc_obj->output_section('docsave::OUTDOC', $doc_obj->get_top_section());
408
409 $self->{'gs_count'}++;
410}
411
412sub get_doc_dir {
413 my $self = shift (@_);
414 my ($OID, $source_filename) = @_;
415
416 my $service = $self-> {'service'};
417
418 my $working_dir = $self->getoutputdir();
419 my $working_info = $self->getoutputinfo();
420 return if (!defined $working_info);
421
422 my $doc_info = $working_info->get_info($OID);
423 my $doc_dir = '';
424
425 if (defined $doc_info && scalar(@$doc_info) >= 1) {
426 # this OID already has an assigned directory, use the
427 # same one.
428 $doc_dir = $doc_info->[0];
429 $doc_dir =~ s/\/?((doc(mets)?)|(dublin_core))\.xml(\.gz)?$//;
430 } elsif ($self->{'keepimportstructure'}) {
431 $source_filename = &File::Basename::dirname($source_filename);
432 $source_filename =~ s/[\\\/]+/\//g;
433 $source_filename =~ s/\/$//;
434
435
436 #print STDERR "Source filename: $source_filename; \nImport dir:",$ENV{'GSDLIMPORTDIR'}, "\n";
437 $doc_dir = substr($source_filename, length($ENV{'GSDLIMPORTDIR'}) + 1);
438
439 }
440 if ($doc_dir eq "") {
441 # have to get a new document directory
442
443 if (($service eq "import") || ($service eq "unbuild")) {
444 my $doc_dir_rest = $OID;
445 my $doc_dir_num = 0;
446
447 do {
448 $doc_dir .= "/" if $doc_dir_num > 0;
449 if ($doc_dir_rest =~ s/^(.{1,8})//) {
450 $doc_dir .= $1;
451 $doc_dir_num++;
452 }
453 } while ($doc_dir_rest ne "" &&
454 ((-d &util::filename_cat ($working_dir, "$doc_dir.dir")) ||
455 ($working_info->size() >= 1024 && $doc_dir_num < 2)));
456 }
457 else {
458 # Export formats such as DSpace need the directory structure to
459 # be flat. This is simple to arrange (set 'doc_dir' to be the
460 # documents OID) but breaks Windows 3.1 file system compliance.
461 # Such a loss is not a big thing in this situation as such
462 # systems don't run on Windows 3.1 anyway.
463
464 $doc_dir = $OID;
465 }
466
467
468 $doc_dir .= ".dir";
469 &util::mk_all_dir (&util::filename_cat ($working_dir, $doc_dir));
470 }
471 return $doc_dir;
472}
473
474sub process_assoc_files {
475 my $self = shift (@_);
476 my ($doc_obj, $doc_dir, $handle) = @_;
477
478 my $outhandle = $self->{'outhandle'};
479 my $service = $self->{'service'};
480 my $save_as = $self->{'saveas'};
481
482 my $output_dir = $self->getoutputdir();
483 return if (!defined $output_dir);
484
485 my $working_dir = &util::filename_cat($output_dir, $doc_dir);
486
487 my @assoc_files = ();
488 my $filename;;
489
490 my $source_filename = $doc_obj->get_source_filename();
491
492 my $collect_dir = $ENV{'GSDLCOLLECTDIR'};
493
494 if (defined $collect_dir) {
495 my $dirsep_regexp = &util::get_os_dirsep();
496
497 if ($collect_dir !~ /$dirsep_regexp$/) {
498 $collect_dir .= &util::get_dirsep(); # ensure there is a slash at the end
499 }
500
501 # This test is never going to fail on Windows -- is this a problem?
502 if ($source_filename !~ /^$dirsep_regexp/) {
503 $source_filename = &util::filename_cat($collect_dir, $source_filename);
504 }
505 }
506
507
508 if ($save_as eq "DSpace") {
509 my ($tail_filename) = ($source_filename =~ m/\/([^\/\\]*)$/);
510
511 print $handle "$tail_filename\n";
512
513 $filename = &util::filename_cat($working_dir, $tail_filename);
514 &util::hard_link ($source_filename, $filename);
515 }
516
517 foreach my $assoc_file_rec (@{$doc_obj->get_assoc_files()}) {
518 my ($dir, $afile) = $assoc_file_rec->[1] =~ /^(.*?)([^\/\\]+)$/;
519 $dir = "" unless defined $dir;
520
521
522 my $real_filename = $assoc_file_rec->[0];
523 if (-e $real_filename) {
524
525
526 if ($save_as eq "DSpace") {
527 if ($real_filename =~ m/$source_filename$/) {
528 next;
529 }
530 else {
531 my $bundle = "bundle:ORIGINAL";
532
533 if ($afile =~ m/^thumbnail\./) {
534 $bundle = "bundle:THUMBNAIL";
535 }
536
537 # Store the associated file to the "contents" file
538 print $handle "$assoc_file_rec->[1]\t$bundle\n";
539 }
540 }
541
542 $filename = &util::filename_cat($working_dir, $afile);
543
544
545 &util::hard_link ($real_filename, $filename);
546
547 $doc_obj->add_utf8_metadata ($doc_obj->get_top_section(),
548 "gsdlassocfile",
549 "$afile:$assoc_file_rec->[2]:$dir");
550 $doc_obj->set_utf8_metadata_element ($doc_obj->get_top_section(),
551 "assocfilepath",
552 "$doc_dir");
553 } elsif ($self->{'verbosity'} > 2) {
554 print $outhandle "docsave::process couldn't copy the associated file " .
555 "$real_filename to $afile\n";
556 }
557 }
558}
559
560
561sub close_file_output
562{
563 my ($self) = @_;
564 my $service =$self->{'service'};
565
566 # make sure that the handle has been opened - it won't be if we failed
567 # to import any documents...
568 if (defined(fileno(docsave::OUTDOC))) {
569 $self->output_xml_footer('docsave::OUTDOC');
570 close OUTDOC;
571 }
572
573 my $OID = $self->{'gs_OID'};
574 my $short_doc_file;
575 # can we use 'short_doc_file' for GA too?
576 if (exists($self->{'saveas'}) && $self->{'saveas'} eq "METS") {
577 $short_doc_file=$self->{'short_doc_file'};
578 } elsif ($self->{'saveas'} eq "GA") { # "GA"
579 $short_doc_file=$self->{'gs_short_filename'};
580 } else { # "DSpace"
581 }
582
583 if ($self->{'gzip'}) {
584 my $doc_file = $self->{'gs_filename'};
585 `gzip $doc_file`;
586 $doc_file .= ".gz";
587 $short_doc_file .= ".gz";
588 if (!-e $doc_file) {
589 my $outhandle = $self->{'outhandle'};
590 print $outhandle "error while gzipping: $doc_file doesn't exist\n";
591 return 0;
592 }
593 }
594
595 # store reference in relevant info object (archive_info,export_info,...)
596 my $output_info = $self->getoutputinfo();
597 return 0 if (!defined $output_info);
598 $output_info->add_info($OID, $short_doc_file);
599
600 return 1;
601}
602
603sub output_xml_header {
604 my $self = shift (@_);
605 my ($handle) = @_;
606
607 print $handle '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' . "\n";
608
609 print $handle '<!DOCTYPE Archive SYSTEM "http://greenstone.org/dtd/Archive/1.0/Archive.dtd">' . "\n";
610 print $handle "<Archive>\n";
611}
612
613sub output_xml_footer {
614 my $self = shift (@_);
615 my ($handle) = @_;
616
617 print $handle "</Archive>\n";
618}
619
620sub output_txt_xml_header{
621 my $self = shift (@_);
622 my ($handle) = @_;
623 print $handle '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' . "\n";
624 print $handle '<!DOCTYPE Archive SYSTEM "http://greenstone.org/dtd/Archive/1.0/Archive.dtd">' . "\n";
625}
626
627sub output_txt_xml_footer{
628 my $self = shift(@_);
629 my ($handle) = @_;
630 # Nothing needs to be output at present
631}
632
633sub output_mets_xml_header(){
634 my $self = shift(@_);
635 my ($handle, $OID, $doc_title) = @_;
636
637 my $version = $self->{'saveas_version'};
638
639 my $extra_attr = "";
640 if ($version eq "fedora") {
641 my $fnamespace = $ENV{'FEDORA_PID_NAMESPACE'};
642 my $oid_namespace = (defined $fnamespace) ? $fnamespace : "test";
643
644 $extra_attr = "OBJID=\"$oid_namespace:$OID\" TYPE=\"FedoraObject\" LABEL=\"$doc_title\"";
645 }
646 else {
647 # Greenstone METS profile
648 $extra_attr = "OBJID=\"$OID:2\"";
649 }
650
651
652 print $handle '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' . "\n";
653 print $handle '<mets:mets xmlns:mets="http://www.loc.gov/METS/"' . "\n";
654 print $handle ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' . "\n";
655 print $handle ' xmlns:gsdl3="http://www.greenstone.org/namespace/gsdlmetadata/1.0/"' . "\n";
656 print $handle ' xmlns:xlink="http://www.w3.org/TR/xlink"' ."\n";
657 print $handle ' xsi:schemaLocation="http://www.loc.gov/METS/' . "\n";
658 print $handle ' http://www.loc.gov/standards/mets/mets.xsd' . "\n";
659 print $handle ' http://www.greenstone.org/namespace/gsdlmetadata/1.0/' . "\n";
660 print $handle ' http://www.greenstone.org/namespace/gsdlmetadata/1.0/gsdl_metadata.xsd"' . "\n";
661 print $handle " $extra_attr>\n";
662
663 if ($version eq "fedora") {
664 print $handle '<mets:metsHdr RECORDSTATUS="A"/>'. "\n"; # A = active
665 }
666
667}
668
669sub output_mets_xml_footer() {
670 my $self = shift(@_);
671 my ($handle) = @_;
672 print $handle '</mets:mets>' . "\n";
673}
674
675sub output_dc_xml_header(){
676 my $self = shift(@_);
677 my ($handle, $OID) = @_;
678
679 print $handle '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' . "\n";
680# print $handle '<!DOCTYPE Archive SYSTEM "http://greenstone.org/dtd/Archive/1.0/Archive.dtd">'."\n";
681 print $handle '<dublin_core>' . "\n";
682}
683
684sub output_dc_xml_footer() {
685 my $self = shift(@_);
686 my ($handle) = @_;
687 print $handle '</dublin_core>' . "\n";
688}
6891;
Note: See TracBrowser for help on using the repository browser.