diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..2a54cb543c85e0f661131768480dd3bbc8efaa08 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +############################################################################### +### ### +### all: compiles all stuff ### +### clean: cleans all stuff ### +### check_prerequisites checks if all prerequisites to compile are fine ### +### distclean: makes Mr Proper clean ### +### build: generates a builded environment, ### +### defaults in ./build/ ### +### buildclean: removes builded environment ### +### force_build: force build also if perl tests failed ### +### install: installs builded environment on system ### +### uninstall: removes strong related stuff from system if part ### +### of previously builded environment ### +############################################################################### + +# If you unsure, the following steps will be a good idea in general: +# $> make check_prerequisites +# $> make clean; build; install + + + + + +PROGRAM=rosettaExitStrategy +PREFIX?=/usr/local/ +PATH_PERL_MOD=perl/ +PATH_SHARE=share/${PROGRAM}/ +PATH_DOC=${PATH_SHARE}/doc/ +PATH_XSL=${PATH_SHARE}/xsl/ +PATH_XSD=${PATH_SHARE}/schema/ +PATH_BIN=bin/ +BUILD?=./build/ +SHELL=/bin/bash + +include ../subprojects.mk + +.PHONY: all clean check_prerequisites distclean doc test uninstall diff --git a/README.1st b/README.1st new file mode 100644 index 0000000000000000000000000000000000000000..f48b1e1ff7759bb57b3378ebb02d38e91bf8eca5 --- /dev/null +++ b/README.1st @@ -0,0 +1,6 @@ +- the textfile 'exit_strategie.asciidoc' is a asciidoc document, it can +be used to generate various output files using asciidoc. As an example +to generate html-output, call: "asciidoc exit_strategie.asciidoc". To get +correctly rendered ER-graphs, the asciidoc filter for 'ditaa' must be +installed. To colorize the code-examples, the 'source-highlight' +programm must be installed as well. diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..28d0f086d7ccd24d9d8f1edb7e42b4b97d338565 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,35 @@ +SOURCES=$(wildcard *.asciidoc) +TARGETS=$(SOURCES:.asciidoc=.html) +SHELL=/bin/bash +RED=\033[0;31m +RESET=\033[0m +GREEN=\033[0;32m + +all: doc + +doc: ${TARGETS} + +%.html: %.asciidoc + asciidoc $< + +clean: + rm -f *.png *.html + +distclean: clean + rm -f *~ + +check_prerequisites: + @echo -n "### Checking asciidoc: ...." + @path=$$(which asciidoc); if [ -e "$${path}" ]; then echo -e " ($${path}) $(GREEN)fine :)$(RESET)"; else echo -e " $(RED)not found! :($(RESET)"; fi + @echo -n "### Checking source-highlight: ...." + @path=$$(which source-highlight); if [ -e "$${path}" ]; then echo -e " ($${path}) $(GREEN)fine :)$(RESET)"; else echo -e " $(RED)not found! :($(RESET)"; fi + @echo -n "### Checking pygmentize: ...." + @path=$$(which pygmentize); if [ -e "$${path}" ]; then echo -e " ($${path}) $(GREEN)fine :)$(RESET)"; else echo -e " $(RED)not found! :($(RESET)"; fi + @echo -n "### Checking ditaa: ...." + @path=$$(which ditaa); if [ -e "$${path}" ]; then echo -e " ($${path}) $(GREEN)fine :)$(RESET)"; else echo -e " $(RED)not found! :($(RESET)"; fi + @echo -n "### Checking asciidoc filter 'ditaa-filter': ...." + @if [[ -e "/etc/asciidoc/filters/ditaa/ditaa-filter.conf" || -e ~/.asciidoc/filters/ditaa/ditaa-filter.conf ]]; then echo -e " ($${path}) $(GREEN)fine :)$(RESET)"; else echo -e " $(RED)not found! :($(RESET)"; fi + + + +.PHONY: all clean check_prerequisites distclean doc diff --git a/doc/exit_strategie.asciidoc b/doc/exit_strategie.asciidoc new file mode 100644 index 0000000000000000000000000000000000000000..e1aa4a7919916fd5946c34785a4a7d736b7bf7f7 --- /dev/null +++ b/doc/exit_strategie.asciidoc @@ -0,0 +1,384 @@ +Exit Strategie Rosetta +====================== +:lang: de +:encoding: utf-8 +:date: 2013-05-09 +:author: Andreas Romeyke +:toc: + +.Erzeugen HTML-Version dieses Dokumentes +[TIP] +=============================================================================== +Nutzen Sie folgenden Aufruf um eine HTML-Version dieses Dokumentes zu +erzeugen: + +[source,bash] +$> asciidoc exit_strategie.txt + +Dies erzeugt die Datei 'exit_strategie.html' +=============================================================================== + + +== Ziel + +Um die Verfügbarkeit der langzeitarchivierten Daten auch bei einem wie auch +immer verursachten Wegfall des Rosetta-Systems sicherzustellen, ist es +notwendig, rechtzeitig Vorsorge zu treffen. + +Im Team wurde dazu folgende Vorgehensweise vereinbart: + +. Perl-Script, welches über das '/permanent_storage' Verzeichnis wandert +. dabei die 'IE.xml' Dateien parst +. und ein SQL-Script produziert +. welches Standard-SQL Befehle für den Aufbau einer Datenbank generiert + +Der Hintergrund der Entscheidung nicht direkt aus dem Perl-Script heraus die +Datenbank zu befeuern (zB. mittels dbi-Treiber) ist, daß man + +. keine Treiber-Module im Perl-Script nachpflegen muß +. falls man eine DB nicht hinbekommt, wenigstens schon eine menschen- und + maschinenlesbare Textdatei hat, die notfalls durchsuchbar ist +. man nicht erst Firewall-Regeln umbiegen muß um dem Script eine + Datenbankverbindung zu ermöglichen + +== Datenbankschema + +Die zu erzeugende Datenbank soll dabei die DublinCore-Elemente der DMD-Section +der AIP-Pakete (aus 'IE.xml' Dateien), den Namen, den tatsächlichen +Speicherpfad der einzelnen Dateien und die Kopie enthalten. + +Aus Performancegründen wird die Lage der 'ie.xml' Dateien in von den sonstigen +Dateien getrennten Tabellen verwaltet. + +Auf die Speicherung der Prüfsummen der Dateien wird verzichtet, da im System 3 +bzw. 4 Kopien der Dateien (inklusive 'ie.xml') vorliegen und so im Falle einer +Datenkorruption beim Ingest ein Mehrheitsentscheid zur Sicherstellung der +Korrektheit ausreichend ist. + +.Entity Relationship Modell +[ditaa] +---------------------------------------------------------------------------- + 1 +---------------+ 1 + +---->| AIP |<----+ + | +->+---------------+ | + | | 1| *ID* (ID) | | + | | | IE_ID (string)| | + | | +---------------+ | +------------------+ + | | | | DC | + | | +---------------+ 1 | +------------------+ ++---------------+ | | | SOURCEDATAFILE|<-+ | n | *ID* (ID) | +| METADATAFILE | | | +---------------+ | +---| AIP_ID (ID) | ++---------------+ | | n| *ID* (ID) | | | ELEMENT (string) | +| *ID* (ID) |n | +--| AIP_ID (ID) | | | VALUE (string) | +| AIP_ID (ID) |-----+ | NAME (string) | | +------------------+ +| LOCATION | +---------------+ | +| (string)| | +| SOURCETYPE | | +| (string)| | ++---------------+ | + +---------------+ | + |SOURCEDATALOCAT| | + +---------------+ | + | *ID* (ID) |n | + | FILE_ID (ID) |--+ + | LOCATION | + | (string)| + | SOURCETYPE | + | (string)| + +---------------+ + +---------------------------------------------------------------------------- + +Es gibt pro AIP-Eintrag in der 'AIP' Tabelle eins oder mehrere 'METADATAFILE', +welches die Lage der ExLibris-Rosetta-METS/MODS Datei beschreiben. Wenn mehrere +Kopien abgelegt sind, gibt es mehrere Einträge. + +Auch die 'SOURCEDATAFILE'-Tabelle beschreibt mehrere Roh-Dateien (zB. die +gescannten TIFFS der einzelnen Buchseiten), deren Kopienspeicherorte aber in +der Tabelle 'SOURCEDATALOCAT' hinterlegt sind. + +Die wichtigsten bibliographischen Metadaten zur Suche sind in der +'DC'-Tabelle hinterlegt. + +[WARNING] +============================================================================ +Da der SQL-Standard keine Angaben zum Erzeugen einer Datenbank macht, +muß das Anlegen einer Datenbank (zB. durch Anweisung 'CREATE +DATABASE…') und die Zuweisung der Benutzerrechte vor dem Einlesen des +Scriptes erfolgen. +============================================================================ + + + +Das Script erzeugt dann SQL-Anweisungen, die pro AIP-Eintrag +als Transaktion geklammert werden. Ein Auszug des erzeugten SQL-Scriptes: + +.SQL-Script +[source,sql] +---------------------------------------------------------------------------- + +BEGIN; +/* create SEQUENCE generator */ +CREATE SEQUENCE serial START 1; +/* create AIP table */ +CREATE TABLE aip ( + id INT PRIMARY KEY DEFAULT nextval('serial'), + ie_id VARCHAR(30) NOT NULL UNIQUE +); +/* create IEFILE table */ +CREATE TABLE metadatafile ( + id INT PRIMARY KEY DEFAULT nextval('serial'), + aip_id INT NOT NULL REFERENCES aip (id), + location VARCHAR(1024) NOT NULL, + sourcetype VARCHAR(30) NOT NULL +); +/* create DC table */ +CREATE TABLE dc ( + id INT PRIMARY KEY DEFAULT nextval('serial'), + aip_id INT NOT NULL REFERENCES aip (id), + element VARCHAR(30) NOT NULL, + value VARCHAR(1024) NOT NULL +); +/* create FILE table */ +CREATE TABLE sourcedatafile ( + id INT PRIMARY KEY DEFAULT nextval('serial'), + aip_id INT NOT NULL REFERENCES aip (id), + name VARCHAR(1024) NOT NULL +); +/* create LOCAT table */ +CREATE TABLE sourcedatalocat ( + id INT PRIMARY KEY DEFAULT nextval('serial'), + file_id INT NOT NULL REFERENCES sourcedatafile (id), + location VARCHAR(1024) NOT NULL, + sourcetype VARCHAR(30) NOT NULL +); +COMMIT; +BEGIN; +PREPARE aip_plan (varchar) AS + INSERT INTO aip (ie_id) VALUES ($1); +PREPARE ie_plan (varchar, varchar, varchar) AS + INSERT INTO metadatafile (aip_id, location, sourcetype) VALUES ( + (SELECT id FROM aip WHERE aip.ie_id=$1), $2, $3 + ); +PREPARE file_plan (varchar, varchar) AS + INSERT INTO sourcedatafile (aip_id, name) VALUES ( + (SELECT id FROM aip WHERE aip.ie_id=$1), $2 + ); +PREPARE locat_plan (varchar, varchar, varchar, varchar) AS + INSERT INTO sourcedatalocat (file_id, location, sourcetype) VALUES ( + (SELECT sourcedatafile.id FROM sourcedatafile,aip WHERE + sourcedatafile.aip_id=aip.id AND aip.ie_id=$1 AND + sourcedatafile.name=$2), $3, $4 + ); +PREPARE dc_plan (varchar, varchar, varchar) AS + INSERT INTO dc (aip_id, element, value) VALUES ( + (SELECT id FROM aip WHERE aip.ie_id=$1), $2, $3 + ); +COMMIT; +BEGIN; +EXECUTE aip_plan ('V1-IE30441'); +EXECUTE ie_plan ('V1-IE30441', 'V1-IE30441.xml', 'hdd'); +EXECUTE file_plan ('V1-IE30441', 'V1-FL30444.tif'); +EXECUTE locat_plan ('V1-IE30441', 'V1-FL30444.tif', '/permanent_storage/file/storage1/2013/03/26/file_1/V1-FL30444.tif', 'hdd' ); +EXECUTE file_plan ('V1-IE30441', 'V1-FL30443.tif'); +EXECUTE locat_plan ('V1-IE30441', 'V1-FL30443.tif', '/permanent_storage/file/storage1/2013/03/26/file_1/V1-FL30443.tif', 'hdd' ); +EXECUTE file_plan ('V1-IE30441', 'V1-FL30446.tif'); +EXECUTE locat_plan ('V1-IE30441', 'V1-FL30446.tif', '/permanent_storage/file/storage1/2013/03/26/file_1/V1-FL30446.tif', 'hdd' ); +EXECUTE file_plan ('V1-IE30441', 'V1-FL30445.tif'); +EXECUTE locat_plan ('V1-IE30441', 'V1-FL30445.tif', '/permanent_storage/file/storage1/2013/03/26/file_1/V1-FL30445.tif', 'hdd' ); +EXECUTE file_plan ('V1-IE30441', 'V1-FL30448.tif'); +EXECUTE locat_plan ('V1-IE30441', 'V1-FL30448.tif', '/permanent_storage/file/storage1/2013/03/26/file_1/V1-FL30448.tif', 'hdd' ); +EXECUTE file_plan ('V1-IE30441', 'V1-FL30447.tif'); +EXECUTE locat_plan ('V1-IE30441', 'V1-FL30447.tif', '/permanent_storage/file/storage1/2013/03/26/file_1/V1-FL30447.tif', 'hdd' ); +EXECUTE file_plan ('V1-IE30441', 'V1-FL30455.xml'); +EXECUTE locat_plan ('V1-IE30441', 'V1-FL30455.xml', '/permanent_storage/file/storage1/2013/03/26/file_1/V1-FL30455.xml', 'hdd' ); +EXECUTE file_plan ('V1-IE30441', 'V1-FL30449.tif'); +EXECUTE locat_plan ('V1-IE30441', 'V1-FL30449.tif', '/permanent_storage/file/storage1/2013/03/26/file_1/V1-FL30449.tif', 'hdd' ); +EXECUTE file_plan ('V1-IE30441', 'V1-FL30454.tif'); +EXECUTE locat_plan ('V1-IE30441', 'V1-FL30454.tif', '/permanent_storage/file/storage1/2013/03/26/file_1/V1-FL30454.tif', 'hdd' ); +EXECUTE file_plan ('V1-IE30441', 'V1-FL30452.tif'); +EXECUTE locat_plan ('V1-IE30441', 'V1-FL30452.tif', '/permanent_storage/file/storage1/2013/03/26/file_1/V1-FL30452.tif', 'hdd' ); +EXECUTE file_plan ('V1-IE30441', 'V1-FL30453.tif'); +EXECUTE locat_plan ('V1-IE30441', 'V1-FL30453.tif', '/permanent_storage/file/storage1/2013/03/26/file_1/V1-FL30453.tif', 'hdd' ); +EXECUTE file_plan ('V1-IE30441', 'V1-FL30450.tif'); +EXECUTE locat_plan ('V1-IE30441', 'V1-FL30450.tif', '/permanent_storage/file/storage1/2013/03/26/file_1/V1-FL30450.tif', 'hdd' ); +EXECUTE file_plan ('V1-IE30441', 'V1-FL30451.tif'); +EXECUTE locat_plan ('V1-IE30441', 'V1-FL30451.tif', '/permanent_storage/file/storage1/2013/03/26/file_1/V1-FL30451.tif', 'hdd' ); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:coverage', 'DE-14'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:coverage', '7.A.1869,angeb.32'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:relation', 'Drucke des 18. Jahrhunderts'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:relation', 'Projekt: Verzeichnis der im deutschen Sprachraum erschienenen Drucke des 18. Jahrhunderts (VD18)'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:identifier', 'oai:de:slub-dresden:db:id-340981210'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:format', '[4] Bl.'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:identifier', '340981210'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:identifier', 'http://digital.slub-dresden.de/id340981210'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:identifier', 'urn:nbn:de:bsz:14-db-id3409812108'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:identifier', '088741990'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:identifier', 'VD18 11664185'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:title', 'Facultatis Iuridicae, Decanus Ernestus Tenzell, J. U. D. Iudicii Provincialis Erfurtensis Assessor, Civitatis Consul Ac Syndicus Primarius ...'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:language', 'la'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:publisher', 'Groschius'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:date', '[1716]'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:subject', 'facuiudee'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:contributor', 'Tentzel, Ernst (Tentzel, Ernst)'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:contributor', 'Talheim, Johann Philipp (Talheim, Johann Philipp)'); +EXECUTE dc_plan ( 'V1-IE30441', 'dc:contributor', '(Deutsche Forschungsgemeinschaft)'); +COMMIT; + +/* INSERT… */ + +-- BEGIN; +-- CREATE UNIQUE INDEX aip_index on aip (ie_id); +-- COMMIT; +---------------------------------------------------------------------------- + + +== Installation + +Das Script ist in Perl 5.14 geschrieben (älterer Perlversionen haben ua. +Probleme mit UTF-8). Es verwendet die Perl-Module 'File::Basename', +'File::Find', 'XML::XPath' und für Debugging 'Data::Dumper'. + +Dem Script ist das Repository-Verzeichnis mitzugeben. Der Aufruf sieht so aus: + +.Beispiel +[source,bash] +---------------------------------------------------------------------------- +$> perl exit_strategy /permanent_storage/ >create_exit_database.sql +---------------------------------------------------------------------------- + +== Beispiel Durchführung Einspielung SQL-Script für Postgres-SQL + +Um unter Postgres-SQL 9.1 die Exitstrategie durchzuführen, sind unter Debian +Wheezy folgende Schritte notwendig footnote::[Benutzer 'exituser' soll +Datenbank 'exit_strategy' gehören]: + +.Beispiel +[source,bash] +---------------------------------------------------------------------------- +$user> sudo aptitude install postgresql +$user> su -c "su -s /bin/sh postgres" +$> createuser -dlr exituser +Soll die neue Rolle ein Superuser sein? (j/n) j +$> createdb exit_strategy -O exituser -E UTF8 +$> exit +---------------------------------------------------------------------------- + +Das Script wird dann so eingespielt: + +.Beispiel +[source,bash] +---------------------------------------------------------------------------- +$user> su -c "su -s /bin/sh postgres" +$> psql -U exituser -d exit_strategy -f exit_strategy.sql \ + -L rosetta_exit.log 2> rosetta_exit.err +---------------------------------------------------------------------------- + +Für weitere Informationen zu Postgres 9.1 siehe +http://www.postgresql.org/docs/9.1/static/ + +== Abschätzungen + +Nach ersten Tests verarbeitet das Perl-Script 277 AIPs in 112s, macht ca. 0,4s +pro AIP. Es wurden dabei ca. 5200 SQL-Anweisungen erzeugt, also ca. 19 pro AIP. +Das erzeugte SQL-File ist 387kB groß, pro AIP fallen also ca. 1,4kB an. + +Bei anvisierten 20 Goobi Vorgängen pro Tag und Exit nach 5 Jahren würden sich +ff. Werte ergeben: 35600 AIPs, Dauer ca. 4h, ca. 68000 SQL Anweisungen, 49 MB +SQL-Datei. + +PostgreSQL benötigte dann 5s um die 277 AIPs aus dem SQL-Script einzulesen, +hochgerechnet wäre die Datenbank dann in 11 min aufgebaut.. + +NOTE: Eine Exit-DB wäre demnach innerhalb von 10h prinzipiell wieder verfügbar. + +== Probleme + +=== UTF-8 Bereiche in Dublincore + +Es ist elementar, daß die Metadaten aus ExLibris-Rosetta sauber sind und alle +Zeichen der verwendeten Dublincore-Felder als UTF-8 aus den Bereichen Basic +Latin (U+0000 => U+007F), Latin-1 Supplement (U+0080 => U+00FF) und +Latin-Extended-A (U+0100 => U+017F) und nicht aus anderen Bereichen stammen. + +Beispielsweise wird das Zeichen '�' (U+FFFD) von Postgres 9.1 abgewiesen. + +In dem Fall muß vor dem Exit eine Metadatenvalidierung innerhalb von +Exlibris-Rosetta durchgeführt werden. In der Regel ist das Vorkommen von +Zeichen außerhalb der oben genannten Bereiche, wie '�' (U+FFFD), ein Hinweis +darauf, daß im Vorfeld ein Problem mit der Konvertierung zwischen UTF-8 und +anderen Zeichenkodierungen vorgelegen hat. + +[NOTE] +==== +Relevant ist ff. Seite: +http://docs.oracle.com/javase/7/docs/api/java/lang/Character.UnicodeBlock.html#forName%28java.lang.String%29[Unicode +Block in Java RegEx] +bzw. +http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html#sum[Unicode +Pattern in Java RegEx] + +Genauer muß geprüft werden, ob ff. Unicode-Block verwendet wird: LATIN_1_SUPPLEMENT + +In RegEx-Notation sieht das Bspw. so aus: + +[source, java] +^[\u0x0000-\u0x00ff]+$ + +==== + +Wichtig ist, daß das PSQL-Kommando auf einer Shell mit aktivierter UTF-8 +Unterstützung genutzt wird, dies kann über die Abfrage 'echo $LANG' geprüft +werden, als Rückgabe sollte 'de_DE.UTF-8' zurückgeliefert werden. + +=== Fehlende Unterstützung mehrfacher Kopien + +Zur Zeit wird nur eine Kopie einer Datei durch das Perl-Script unterstützt. +Sobald klar ist, wie diese Informationen in den AIP-Paketen hinterlegt sind, +sollte das Perl-script daran angepasst werden. + +== Anmerkungen seitens ExLibris Rosetta + +Leider ist es zur Zeit so, daß seitens ExLibris noch keine offizielle +Dokumentation zur Rosetta eigenen Datenablage der AIPs im '/permanent_storage' +vorhanden ist. + +Allerdings hat ExLibris auf einen Support Incident wie folgt geantwortet: + +.Auszug aus Incident #16384-420304 SI Name: Overview / Explanation of AIP relevant files - information is requested +[NOTE] +==== +As you know from SI 16384-418600: "All AIPs (a.k.a. Intellectual Entities) +metadata including audit (provenance) information is stored on the disk in +Rosetta METS format." The actual place of the IE Rosetta METS XML files in the +file system is configured in the storage rules and definitions. + +Home > Advanced Configuration > Repository > Storage Rules and Definitions +> Storage Group List > IE Group + +Storage media that contains the IE METS files + +For example, on your staging server the NFS path to the storage 1 is +'/permanent_storage/ie/storage1'. + +The configured storages have the same structure: + +'<root path><storage group><storage>/<year>/<month>/<day>/<1-999 numbered +subdirs with prefix>/' + +Example: +'/permanent_storage/ie/storage1/2013/03/26/file_1/' + +Here you find all versions of the IE Rosetta METS XML files, e.g. +'/permanent_storage/ie/storage1/2013/03/26/file_1/V1-IE31220.xml' + +The prefix 'V1-' indicates that this is version #1 of the IE Rosetta METS XML +file. The link to the actual file streams is in the XML in the streamref +section. You have to make sure that you are using the highest (i.e. latest) +version of the METS. + +With the information above you can develop an exit strategy which parses all +IE storage directories to find the IEs and their related file streams. +==== + + diff --git a/perl/Makefile b/perl/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..658624f2a30fe80e6e0289854432054aa470f3a4 --- /dev/null +++ b/perl/Makefile @@ -0,0 +1,50 @@ +REQUIRED_MODULES=$(shell find ./ \( -name "*.p[ml]" -o -name "*.t" \) -exec grep "^use \([A-Za-z][A-Za-z0-9:]\+\)" \{\} \; | sed -e "s/^use \([A-Za-z][A-Za-z0-9:]\+\).*/\1/g" | sort | uniq) +REQUIRED_SYSMODULES=$(shell find ./ \( -name "*.p[ml]" -o -name "*.t" \) -exec grep "^use \([A-Za-z][A-Za-z0-9:]\+\)" \{\} \; | grep -v "SLUB::LZA" | grep "^use [A-Z]" | sed -e "s/^use \([A-Za-z][A-Za-z0-9:]\+\).*/\1/g" | sort | uniq) +PMs=$(shell find ./ -name "*.pm") +PODs=$(PMs:.pm=.pod) + +all: perl_tests_ok + +clean: + rm -f perl_tests_ok + +distclean: clean + find ./ -name "*~" | xargs rm -f + find ./ -name "*.pod" | xargs rm -f + rm -Rf testdir/ + rm -Rf cover_db/ + +find_subs: + find ./ -name "*.pm" -exec grep -E "^\s*sub\s+(\w+)\b" \{\} \; + +find_vars: + find ./ -name '*.pm' -exec grep 'my \$$[A-Za-z0-9_]' \{\} \; | egrep -o '\$$[A-Za-z0-9_]+' | sort | uniq + +check_prerequisites: + @for i in $(REQUIRED_MODULES); do\ + echo -n "### Checking if Perl-Module '$$i' exists ..." ;\ + /usr/bin/perl -I./ -e "use Term::ANSIColor; if (eval {require $$i; 1;} ne 1) { print color 'bold red'; print \" not found! :(\n\";} else {print color 'green'; print \"fine! :)\n\";}; print color 'reset';";\ + done + +perl_tests_ok: + touch perl_tests_ok + @true + +list_required_perl_modules: + @for i in $(REQUIRED_SYSMODULES) ; do\ + echo $$i ;\ + done + +# autpod is part of Pod::Autopod +%.pod:%.pm + autopod -r $< --pod -w $@ + +doc: $(PODs) + +#cover: clean +# PERL5OPT=-MDevel::Cover $(MAKE) perl_tests_ok +# cover -ignore_re '.*\.t' -ignore_re '.*prove' +# @echo report found in cover_db/coverage.html + +.PHONY: all clean check_prerequisites doc distclean find_subs list_required_perl_modules perl_tests_ok + diff --git a/perl/exit_strategy.pl b/perl/exit_strategy.pl new file mode 100644 index 0000000000000000000000000000000000000000..367e0384d8c6c45ced765225544f9497fb59ea97 --- /dev/null +++ b/perl/exit_strategy.pl @@ -0,0 +1,365 @@ +#!/usr/bin/perl -w +############################################################################### +# Author: Andreas Romeyke +# SLUB Dresden, Department Longterm Preservation +# +# scans a given repository and creates an SQL script to create a database. +# This is part of the exit-strategy for details, see asciidoc file +# exit_strategie.asciidoc (also contains ER-diagram for database) +# +# file tested with postgres-database +# +# using: +# psql -U romeyke -d exit_strategy \ +# -f rosetta_exit_strategy/tmp.sql -L rosetta_exit.log +# +############################################################################### + +use 5.14.0; +use strict; +use warnings; +use Carp; +use File::Basename; +use File::Find; +use XML::XPath; +use XML::XPath::XMLParser; + +# guarantee, that output will be UTF8 +binmode(STDOUT, ":encoding(UTF-8)"); +my $db_name="exit_strategy"; +my $schema_name="exit_strategy"; +my $sourcetype="hdd"; #default value + +############################################################################### +# write database creation +# write tables creation +# scan repository +# if IE.xml file found, read its metadata, create SQL add entry +# write SQL add entry +############################################################################### +sub write_database_creation { + # non standard conform SQL keywords + #say "CREATE DATABASE $db_name;"; + #say "CREATE SCHEMA $schema_name;"; + #say "USE "; +} + +# write tables creation;: +sub write_tables_creation { + # Transactions for tables creation + say "BEGIN;"; + + # SEQUENCE + say "/* create SEQUENCE generator */"; + say "CREATE SEQUENCE serial START 1;"; + + # AIP + say "/* create AIP table */"; + say "CREATE TABLE aip ("; + say "\tid INT PRIMARY KEY DEFAULT nextval('serial'),"; + say "\tie_id VARCHAR(30) NOT NULL UNIQUE"; + say ");"; + # IEFILE + say "/* create IEFILE table */"; + say "CREATE TABLE metadatafile ("; + say "\tid INT PRIMARY KEY DEFAULT nextval('serial'),"; + say "\taip_id INT NOT NULL REFERENCES aip (id),"; + say "\tlocation VARCHAR(1024) NOT NULL,"; + say "\tsourcetype VARCHAR(30) NOT NULL"; + say ");"; + # DC + say "/* create DC table */"; + say "CREATE TABLE dc ("; + say "\tid INT PRIMARY KEY DEFAULT nextval('serial'),"; + say "\taip_id INT NOT NULL REFERENCES aip (id),"; + say "\telement VARCHAR(30) NOT NULL,"; + say "\tvalue VARCHAR(1024) NOT NULL"; + say ");"; + # FILE + say "/* create FILE table */"; + say "CREATE TABLE sourcedatafile ("; + say "\tid INT PRIMARY KEY DEFAULT nextval('serial'), "; + say "\taip_id INT NOT NULL REFERENCES aip (id),"; + say "\tname VARCHAR(1024) NOT NULL"; + say ");"; + # LOCAT + say "/* create LOCAT table */"; + say "CREATE TABLE sourcedatalocat ("; + say "\tid INT PRIMARY KEY DEFAULT nextval('serial'),"; + say "\tfile_id INT NOT NULL REFERENCES sourcedatafile (id),"; + say "\tlocation VARCHAR(1024) NOT NULL,"; + say "\tsourcetype VARCHAR(30) NOT NULL"; + say ");"; + #end transaction + say "COMMIT;"; + return; +} + +############################################################################### +# Prepare SQL INSERT Statements for AIPs +############################################################################### +sub write_prepare_insert { + say "BEGIN;"; + say "PREPARE aip_plan (varchar) AS"; + say " INSERT INTO aip (ie_id) VALUES (\$1);"; + say "PREPARE ie_plan (varchar, varchar, varchar) AS"; + say " INSERT INTO metadatafile (aip_id, location, sourcetype) VALUES ("; + say " (SELECT id FROM aip WHERE aip.ie_id=\$1), \$2, \$3"; + say " );"; + say "PREPARE file_plan (varchar, varchar) AS"; + say " INSERT INTO sourcedatafile (aip_id, name) VALUES ("; + say " (SELECT id FROM aip WHERE aip.ie_id=\$1), \$2"; + say " );"; + say "PREPARE locat_plan (varchar, varchar, varchar, varchar) AS"; + say " INSERT INTO sourcedatalocat (file_id, location, sourcetype) VALUES ("; + say " (SELECT sourcedatafile.id FROM sourcedatafile,aip WHERE"; + say " sourcedatafile.aip_id=aip.id AND aip.ie_id=\$1 AND"; + say " sourcedatafile.name=\$2), \$3, \$4"; + say " );"; + say "PREPARE dc_plan (varchar, varchar, varchar) AS"; + say " INSERT INTO dc (aip_id, element, value) VALUES ("; + say " (SELECT id FROM aip WHERE aip.ie_id=\$1), \$2, \$3"; + say " );"; + say "COMMIT;"; + return; +} + + +############################################################################### +# write add SQL entry, expects a hashref which contains ff. params +# (foreach file location/copy): +# INSERT INTO aip (ie_id) VALUES ($ieid); +# INSERT INTO iefile (aip_id, location, sourcetype) VALUES ( +# (SELECT id FROM aip where aip.ieid = $ieid), $location, $sourcetype); +# INSERT INTO file (aip_id, name) VALUES ( +# (SELECT id FROM aip where aip.ieid = $ieid), $name); +# INSERT INTO locat (file_id, location, sourcetype) VALUES ( +# (SELECT file.aip_id FROM file where file.aip_id = aip.id +# AND aip.ie_id=$ieid), $location, $sourcetype) +# INSERT INTO dc (aip_id, element, value) VALUES ( +# (SELECT id FROM aip where aip.ieid = $ieid), $element, $value); +# TODO: needs additional work +# expects a reference of an hash: +# $ret{"filename" } = $filename; +# $ret{"title"} = $title; +# $ret{"repid"} = $repid; +# $ret{"files"} = \@files; +# $ret{"dcrecords"} = \@dcrecords; +############################################################################### +sub write_addsql { + my $refhash = $_[0]; + my $ieid = basename($refhash->{"filename"},qw/.xml/); + say "BEGIN;"; + say "EXECUTE aip_plan ('$ieid');"; + # FIXME if multiple locations exists + my $iefile = basename($refhash->{"filename"}); + say "EXECUTE ie_plan ('$ieid', '$iefile', '$sourcetype');"; + foreach my $location (@{$refhash->{"files"}}) { + my $file = basename($location); # FIXME if multiple locations + my $dir = dirname($location); + say "EXECUTE file_plan ('$ieid', '$file');"; + say "EXECUTE locat_plan ('$ieid', '$file', '$location', '$sourcetype' );"; + } + foreach my $dcpair (@{$refhash->{"dcrecords"}}) { + my ($dckey,$dcvalue) = @{$dcpair}; + # quote ' in dcvalue + $dcvalue=~tr/'/"/; + say "EXECUTE dc_plan ( '$ieid', '$dckey', '$dcvalue');"; + } + say "COMMIT;"; + say "\n"; + return; +} + + + +############################################################################### +# add INDEX and other TRICKs to increase performance +############################################################################### +sub write_index_creation() { + say "-- BEGIN;"; + say "-- CREATE UNIQUE INDEX aip_index on aip (ie_id);"; + say "-- COMMIT;"; + return; +} + +############################################################################### +# checks if a given string from from a given file contains only utf-8 chars +# which are compatible to common used databases +############################################################################### +sub check_if_db_conform ($$) { + my $string = "$_[0]"; + my $filename = $_[1]; + if ($string ne '') { + if ( not utf8::is_utf8($string)) { + croak "no utf8: '$string' in file '$filename'\n"; + } + }# + return; +} + + +############################################################################### +# +# /mets:mets/mets:dmdSec[1]/mets:mdWrap[1]/mets:xmlData[1]/dc:record[1]/dc:title[1] +# /mets:mets/mets:amdSec[1]/mets:techMD[1]/mets:mdWrap[1]/mets:xmlData[1]/dnx[1]/section[1]/record[1]/key[2] +# mit ID=Label und Wert = LOCAL +# dort die ID von techMD (Referenz für Files) +# +# Files via /mets:mets/mets:fileSec[1]/mets:fileGrp[1]/mets:file[1]/mets:FLocat[1] +# +############################################################################### +sub parse_iexml { + my $filename = $_[0]; + # create object + my $xp = XML::XPath->new (filename => $filename); + ############################################ + # get title + my $title = $xp->findvalue('/mets:mets/mets:dmdSec/mets:mdWrap[1]/mets:xmlData[1]/dc:record/dc:title[1]'); + check_if_db_conform($title, $filename); + ############################################ + # get dc-records + my @dcrecords; + my $dcnodes = $xp->find('/mets:mets/mets:dmdSec/mets:mdWrap/mets:xmlData/dc:record/*'); + foreach my $dcnode ($dcnodes->get_nodelist) { + my $key = $dcnode->getName("."); + my $value = $dcnode->findvalue("."); + if (defined $value) { + $value=~s/\n/ /g; + $value=~s/'/\\'/g; + } + check_if_db_conform ($value, $filename); + my @pair; + push @pair, $key; + push @pair, $value; + push @dcrecords, \@pair; + } + ############################################ + # get right representation ID (has a dnx-section with <key id=label>LOCAL</key>) + my $repids = $xp->find('/mets:mets/mets:amdSec'); + my $repid; + # FIXME: if only one represenation exists (Qucosa), select this. If there + # are more than one, use them with label LOCAL + my @repnodes = $repids->get_nodelist; + + $repid = $repnodes[0]->findvalue('@ID' ); + foreach my $node (@repnodes) { + my $id = $node->findvalue('@ID' ); + check_if_db_conform($id, $filename); + #/mets:mets/mets:amdSec[1]/mets:techMD[1]/mets:mdWrap[1]/mets:xmlData[1]/dnx[1]/section[1]/record[1]/key[1] + # + if ($node->findvalue('mets:techMD/mets:mdWrap/mets:xmlData/dnx/section/record/key[@id=\'label\']') eq 'LOCAL') { + $repid=$id; + } + #print XML::XPath::XMLParser::as_string($node), "\n\n"; + } + ############################################ + # get all files of LOCAL representation + my @files; + my $filegrpnodes = $xp->find('/mets:mets/mets:fileSec/mets:fileGrp'); + foreach my $filegrpnode ($filegrpnodes->get_nodelist) { + #die XML::XPath::XMLParser::as_string($filegrpnode), "\n\n"; + #die Dumper($filegrpnode); + if ($filegrpnode->findvalue('@ADMID') eq $repid) { + #die Dumper($filegrpnode); + my $filesnodes = $filegrpnode ->find("mets:file/mets:FLocat"); + foreach my $filesnode ($filesnodes->get_nodelist) { + my $value = $filesnode->findvalue('@xlin:href'); + check_if_db_conform($value, $filename); + push @files, sprintf("%s", $value); + } + } + } + my %ret; + $ret{"filename" } = $filename; + $ret{"title"} = $title; + $ret{"repid"} = $repid; + $ret{"files"} = \@files; + $ret{"dcrecords"} = \@dcrecords; + return \%ret; +} + +############################################################################### +# because ExLibris Rosetta produces filenames of following format: +# V\d+-IE\d+\.xml +# e.G.: +# V1-IE23891.xml +# V1-IE94621.xml +# V2-IE23891.xml +# … +# we must find the relevant file with highest V-value, in example the file +# "V2-IE23891.xml" +# +# this function gets an array reference with all possible files of given regEx +# and returns an array reference with reduced files using only highest V-value +################################################################################ +sub find_newest_iefile_version ($) { + my $files = $_[0]; + #say "$files="; + #say Dumper($files); + my %fileshash; + foreach my $file (@{ $files } ) { + $file=~m/^(.+?V)(\d+)(-IE\d+\.xml)$/; + my ($prefix, $version, $suffix) = ($1, $2, $3); + if (defined $fileshash{$suffix}) { + my ($stored_version, $stored_prefix) = @{ $fileshash{$suffix} }; + if ($version > $stored_version) { + carp "replaced $stored_version with $version of $suffix"; + my @tmp = ($version, $prefix); + $fileshash{$suffix} = \@tmp; + } + } else { + my @tmp = ($version, $prefix); + $fileshash{$suffix} = \@tmp; + } + } + # build new array + my @newfiles = sort { $a eq $b } map { + my $suffix=$_; + my ($version, $prefix) = @{ $fileshash{ $suffix } }; + join ("", $prefix, $version, $suffix); + } (keys %fileshash); + #say "filtered $files="; + #say Dumper(\@newfiles); + return \@newfiles; +} + +# begin closure +{ + my @files; +############################################################################### +# call back function to File::Find +# +############################################################################### + sub process_sip () { + my $file=$File::Find::name; + if ($file =~ m/V\d+-IE\d+\.xml$/) { + push @files, $file; + } + return; + } +############################################################################### +############################################################################### +############# main ############################################################ +############################################################################### +############################################################################### + my $dir = shift @ARGV; + if (defined $dir && -d "$dir") { + write_database_creation(); + write_tables_creation(); + write_prepare_insert(); + find(\&process_sip, $dir); + # find newest version of files + my @sorted_files = sort {$a eq $b} @files; + my $files = find_newest_iefile_version ( \@sorted_files ); + foreach my $file (@{ $files }) { + my $ret = parse_iexml($file); + write_addsql($ret); + } + write_index_creation(); + } else { + die "no directory given on commandline" + } +} #end closure +1; +