]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/support-scripts/marc_stream_importer.pl
Implement tempdir, tempfile option.
[Evergreen.git] / Open-ILS / src / support-scripts / marc_stream_importer.pl
1 #!/usr/bin/perl
2 # Copyright (C) 2008 Equinox Software, Inc.
3 # Author: Bill Erickson <erickson@esilibrary.com>
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14
15
16 use strict; use warnings;
17 use Net::Server::PreFork;
18 use base qw/Net::Server::PreFork/;
19 use MARC::Record;
20 use MARC::Batch;
21 use MARC::File::XML;
22 use MARC::File::USMARC;
23
24 use Data::Dumper;
25 use File::Basename qw/fileparse/;
26 use File::Temp;
27 use Getopt::Long qw(:DEFAULT GetOptionsFromArray);
28 use Pod::Usage;
29
30 use OpenSRF::Utils::Logger qw/$logger/;
31 use OpenILS::Utils::Cronscript;
32 require 'oils_header.pl';
33 use vars qw/$apputils/;
34
35 my $debug = 0;
36
37 my %defaults = (
38     'buffsize=i'    => 4096,
39     'merge=i'       => 0,
40     'source=i'      => 1,
41 #    'osrf-config=s' => '/openils/conf/opensrf_core.xml',
42     'user=s'        => 'admin',
43     'password=s'    => '',
44     'tempdir=s'     => '',
45     'nolockfile'    => 1,
46     'queue'         => 1,
47     'noqueue'       => 0,
48     'wait=i'        => 5,
49 );
50
51 $OpenILS::Utils::Cronscript::debug=1 if $debug;
52 $Getopt::Long::debug=1 if $debug > 1;
53 my $o = OpenILS::Utils::Cronscript->new(\%defaults);
54
55 my @script_args = ();
56
57 if (grep {$_ eq '--'} @ARGV) {
58     print "Splitting options into groups\n" if $debug;
59     while (@ARGV) {
60         $_ = shift @ARGV;
61         $_ eq '--' and last;    # stop at the first --
62         push @script_args, $_;
63     }
64 } else {
65     @script_args = @ARGV;
66     @ARGV = ();
67 }
68
69 print "Calling MyGetOptions ",
70     (@script_args ? "with options: " . join(' ', @script_args) : 'without options from command line'),
71     "\n" if $debug;
72
73 my $real_opts = $o->MyGetOptions(\@script_args);
74 $o->bootstrap;
75 # GetOptionsFromArray(\@script_args, \%defaults, %defaults); # similar to
76
77 $real_opts->{tempdir} ||= tempdir_setting();    # This doesn't go in defaults because it reads config, must come after bootstrap
78
79 my $bufsize       = $real_opts->{buffsize};
80 my $bib_source    = $real_opts->{source};
81 my $osrf_config   = $real_opts->{'osrf-config'};
82 my $oils_username = $real_opts->{user};
83 my $oils_password = $real_opts->{password};
84 my $help          = $real_opts->{help};
85 my $merge_profile = $real_opts->{merge_profile};
86 my $queue_id      = $real_opts->{queue};
87 my $tempdir       = $real_opts->{tempdir};
88    $debug        += $real_opts->{debug};
89
90 foreach (keys %$real_opts) {
91     print("real_opt->{$_} = ", $real_opts->{$_}, "\n") if $real_opts->{debug} or $debug;
92 }
93 my $wait_time     = $real_opts->{wait};
94 my $authtoken     = '';
95
96 # DEFAULTS for Net::Server
97 my $filename   = fileparse($0, '.pl');
98 my $conf_file  = (-r "$filename.conf") ? "$filename.conf" : undef;
99 # $conf_file is the Net::Server config for THIS script (not EG), if it exists and is readable
100
101
102 # FEEDBACK
103
104 pod2usage(1) if $help;
105 unless ($oils_password) {
106     print STDERR "\nERROR: password option required for session login\n\n";
107     # pod2usage(1);
108 }
109
110 print Dumper($o) if $debug;
111
112 if ($debug) {
113     foreach my $ref (qw/bufsize bib_source osrf_config oils_username oils_password help conf_file debug/) {
114         no strict 'refs';
115         printf "%16s => %s\n", $ref, (eval("\$$ref") || '');
116     }
117 }
118
119 print warning();
120 print Dumper($real_opts);
121
122 # SUBS
123
124 sub tempdir_setting {
125     my $ret = $apputils->simplereq( qw# opensrf.settings opensrf.settings.xpath.get
126         /opensrf/default/apps/open-ils.vandelay/app_settings/databases/importer # );
127     return $ret->[0] || '/tmp';
128 }
129
130 sub warning {
131     return <<WARNING;
132
133 WARNING:  This script provides no security layer.  Any client that has 
134 access to the server+port can inject MARC records into the system.  
135
136 WARNING
137 }
138
139 sub xml_import {
140     return $apputils->simplereq(
141         'open-ils.cat', 
142         'open-ils.cat.biblio.record.xml.import',
143         @_
144     );
145 }
146
147 sub old_process_batch_data {
148     my $data = shift or $logger->error("process_batch_data called without any data");
149     $data or return;
150
151     my $handle;
152     open $handle, '<', \$data; 
153     my $batch = MARC::Batch->new('USMARC', $handle);
154     $batch->strict_off;
155
156     my $index = 0;
157     while (1) {
158         my $rec;
159         $index++;
160
161         eval { $rec = $batch->next; };
162
163         if ($@) {
164             $logger->error("Failed parsing MARC record $index");
165             next;
166         }
167         last unless $rec;   # The only way out
168
169         my $resp = xml_import($authtoken, $rec->as_xml_record, $bib_source);
170
171         # has the session timed out?
172         if (oils_event_equals($resp, 'NO_SESSION')) {
173             new_auth_token();
174             $resp = xml_import($authtoken, $rec->as_xml_record, $bib_source);   # try again w/ new token
175         }
176         oils_event_die($resp);
177     }
178     return $index;
179 }
180
181 sub process_spool {     # filename
182     $apputils->simplereq('open-ils.vandelay', 'open-ils.vandelay.bib.process_spool', $authtoken, undef,
183                          $queue_id, 'import', shift, $bib_source );
184 }
185 sub bib_queue_import {
186     my $extra = {auto_overlay_exact => 1};
187     $extra->{merge_profile} = $merge_profile if $merge_profile;
188     $apputils->simplereq('open-ils.vandelay', 'open-ils.vandelay.bib_queue.import', $authtoken,
189                          $queue_id, $extra );
190 }
191
192 sub process_batch_data {
193     my $data = shift or $logger->error("process_batch_data called without any data");
194     $data or return;
195
196     my ($handle, $tempfile) = File::Temp(DIR => $tempdir) or die "Cannot write tempfile in $tempdir";
197     print $handle $data;
198     close $handle;
199        
200     my $resp = process_spool($tempfile);
201
202     if (oils_event_equals($resp, 'NO_SESSION')) {  # has the session timed out?
203         new_auth_token();
204         $resp = process_spool($tempfile);                # try again w/ new token
205     }
206
207     $resp = bib_queue_import();
208
209     if (oils_event_equals($resp, 'NO_SESSION')) {  # has the session timed out?
210         new_auth_token();
211         $resp = bib_queue_import();                # try again w/ new token
212     }
213     oils_event_die($resp);
214 }
215
216 sub process_request {   # The core Net::Server method
217     my $self = shift;
218     my $socket = $self->{server}->{client};
219     my $data = '';
220     my $buf;
221
222     # Reading <STDIN> blocks until the client is closed.  Instead of waiting 
223     # for that, give each inbound record $wait_time seconds to fully arrive
224     # and pull the data directly from the socket
225     eval {
226         local $SIG{ALRM} = sub { die "alarm\n" }; 
227         do {
228             alarm $wait_time;
229             last unless $socket->sysread($buf, $bufsize);
230             $data .= $buf;
231         } while(1);
232         alarm 0;
233     };
234     if ($real_opts->{noqueue}) {
235         old_process_batch_data($data);
236     } else {
237         process_batch_data($data);
238     }
239 }
240
241
242 # the authtoken will timeout after the configured inactivity period.
243 # When that happens, get a new one.
244 sub new_auth_token {
245     $authtoken = oils_login($oils_username, $oils_password, 'staff') 
246         or die "Unable to login to Evergreen as user $oils_username";
247     return $authtoken;
248 }
249
250 ##### MAIN ######
251
252 osrf_connect($osrf_config);
253 new_auth_token();
254 print "Calling Net::Server run ", (@ARGV ? "with command-line options: " . join(' ', @ARGV) : ''), "\n";
255 __PACKAGE__->run(conf_file => $conf_file);
256
257 __END__
258
259 =head1 NAME
260
261 marc_stream_importer.pl - Import MARC records via bare socket connection.
262
263 =head1 SYNOPSIS
264
265 ./marc_stream_importer.pl [common opts ...] [script opts ...] -- [Net::Server opts ...] &
266
267 This script uses the EG common options from B<Cronscript>.  See --help output for those.
268
269 Run C<perldoc marc_stream_importer.pl> for full documentation.
270
271 Note the extra C<--> to separate options for the script wrapper from options for the
272 underlying L<Net::Server> options.  
273
274 Note: this script has to be run in the same directory as B<oils_header.pl>.
275
276 Typical execution will include a trailing C<&> to run in the background.
277
278 =head1 DESCRIPTION
279
280 This script is a L<Net::Server::PreFork> instance for shoving records into Evergreen from a remote system.
281
282 =head1 OPTIONS
283
284 The only required option is --password
285
286  --password =<eg_password>
287  --user     =<eg_username>   default: admin
288  --source   =<bib_source>    default: 1         Integer
289  --merge    =<i>             default: 0
290  --tempdir  =</temp/dir/>    default: from L<opensrf.conf> <open-ils.vandelay/app_settings/databases/importer>
291  --source   =<i>             default: 1
292
293 =head2 Old style: --noqueue and associated options
294
295 To bypass vandelay queue processing and push directly into the database (as the old style)
296
297  --noqueue         default: OFF
298  --buffsize =<i>   default: 4096    Buffer size.  Only used by --noqueue
299  --wait     =<i>   default: 5       Seconds to read socket before processing.  Only used by --noqueue
300
301 =head2 Net::Server Options
302
303 By default, the script will use the Net::Server configuration file B<marc_stream_importer.conf>.  You can 
304 override this by passing a filepath with the --conf_file option.
305
306 Other Net::Server options include: --port=<port> --min_servers=<X> --max_servers=<Y> and --log_file=[path/to/file]
307
308 See L<Net::Server> for a complete list.
309
310 =head2 Configuration
311
312 =head3 OCLC Connexion
313
314 To use this script with OCLC Connexion, configure the client as follows:
315
316 Under Tools -> Options -> Export (tab)
317    Create -> Choose Connection -> OK -> Leave translation at "None" 
318        -> Create -> Create -> choose TCP/IP (internet) 
319        -> Enter hostname and Port, leave 'Use Telnet Protocol' checked 
320        -> Create/OK your way out of the dialogs
321    Record Characteristics (button) -> Choose 'UTF-8 Unicode' for the Character Set
322    
323
324 OCLC and Connexion are trademark/service marks of OCLC Online Computer Library Center, Inc.
325
326 =head1 CAVEATS
327
328 WARNING: This script provides no inherent security layer.  Any client that has 
329 access to the server+port can inject MARC records into the system.  
330 Use the available options (like allow/deny) in the Net::Server config file 
331 or via the command line to restrict access as necessary.
332
333 =head1 EXAMPLES
334
335 ./marc_stream_importer.pl  \
336     admin open-ils connexion --port 5555 --min_servers 2 \
337     --max_servers=20 --log_file=/openils/var/log/marc_net_importer.log &
338
339 =head1 SEE ALSO
340
341 L<Net::Server::PreFork>, L<marc_stream_importer.conf>
342
343 =head1 AUTHORS
344
345     Bill Erickson <erickson@esilibrary.com>
346     Joe Atzberger <jatzberger@esilibrary.com>
347
348 =cut