Initial commit
This commit is contained in:
234
docs/02-export-aus-ecodms.md
Normal file
234
docs/02-export-aus-ecodms.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# 2. Export aus ecoDMS
|
||||
|
||||
Der Export läuft über das **Datenexport-Plugin**, das zum normalen
|
||||
Lizenzumfang gehört. Eine API-Lizenz wird nicht gebraucht.
|
||||
|
||||
---
|
||||
|
||||
## Warum nicht die Datenbank
|
||||
|
||||
Naheliegend wäre, die ecoDMS-PostgreSQL direkt auszulesen. Das
|
||||
funktioniert nicht: ecoDMS legt die Dateien in einem eigenen
|
||||
Container-Speichersystem ab, weder als BLOB in der Datenbank noch als
|
||||
normale Dateien im Archivverzeichnis.
|
||||
|
||||
Und die Klassifizierungsdaten liegen dort ebenfalls nicht — in der
|
||||
Live-Datenbank gibt es weder eine `klassifizierung`- noch eine
|
||||
`docs`-Tabelle. Diese Struktur erzeugt erst der Exporter.
|
||||
|
||||
Der Export ist also der einzige Weg an Inhalte **und** Metadaten.
|
||||
|
||||
---
|
||||
|
||||
## Vorab: Bestandsaufnahme
|
||||
|
||||
Bevor Zeit in Exporte fließt, sollte man die Größenordnung kennen. Am
|
||||
einfachsten über einen kleinen Testexport (siehe unten) und die Auswertung
|
||||
der Tabellen `ecosimsversions` und `docs`.
|
||||
|
||||
Wichtige Zahlen:
|
||||
|
||||
| Frage | Warum |
|
||||
|---|---|
|
||||
| Wie viele Dokumente haben mehr als eine Version? | bestimmt den Aufwand des schwierigen Teils |
|
||||
| Wie viele haben **keine** Version? | brauchen einen Rückfall auf `ecosimsarchive` |
|
||||
| Wie viele sind mehrfach klassifiziert? | bestimmt die Zusammenführungsregel |
|
||||
| Welche Dateitypen kommen vor? | nicht alle kann Paperless konsumieren |
|
||||
| Welche Rollen und Rechtearten? | müssen in der Übersetzung abgebildet sein |
|
||||
|
||||
`ecodms_extract.py` beantwortet all das im Prüfbericht — es lohnt sich
|
||||
deshalb, früh einen kleinen Export zu machen und ihn durch Stufe 1 zu
|
||||
schicken.
|
||||
|
||||
---
|
||||
|
||||
## Testexport zuerst
|
||||
|
||||
**Nicht mit dem Gesamtbestand anfangen.** Ein Export von fünf bis zehn
|
||||
Dokumenten, der bewusst alle Sonderfälle enthält, ist die beste
|
||||
Investition:
|
||||
|
||||
- ein Office-Dokument mit mehreren Versionen
|
||||
- ein PDF mit mehreren Versionen
|
||||
- ein Dokument ganz **ohne** Versionseinträge
|
||||
- ein mehrfach klassifiziertes Dokument
|
||||
- eine E-Mail (`.eml`)
|
||||
- eine ZUGFeRD-Rechnung
|
||||
- ein Dokument mit einer Notiz
|
||||
- ein Dokument mit besonderen Rechten
|
||||
|
||||
Gegen diesen Satz lässt sich die gesamte Migrationslogik entwickeln, ohne
|
||||
je den Produktivbestand anzufassen.
|
||||
|
||||
---
|
||||
|
||||
## Fallstrick: unerfüllbare Exportabfrage
|
||||
|
||||
Bei manueller Mehrfachauswahl kann der Exporter eine Abfrage erzeugen, die
|
||||
niemals zutrifft:
|
||||
|
||||
```sql
|
||||
docid = '11072' AND docid = '11094' AND docid = '11143' AND …
|
||||
```
|
||||
|
||||
Das Ergebnis: ein Export mit vollständigen Stammdaten und **null
|
||||
Dokumenten**. Ohne Fehlermeldung, ohne Warnung.
|
||||
|
||||
**Nach jedem Export prüfen:**
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import sqlite3, base64
|
||||
c = sqlite3.connect('file:export.data?mode=ro', uri=True)
|
||||
print(base64.b64decode(c.execute('SELECT exp_query FROM ecodmsexporter').fetchone()[0]).decode())
|
||||
"
|
||||
```
|
||||
|
||||
Steht dort `AND` zwischen den IDs statt `OR`, ist der Export leer.
|
||||
|
||||
Vermeiden lässt es sich, indem man Exporte über ein **Suchkriterium** oder
|
||||
einen **ID-Bereich** definiert statt über einzeln markierte Dokumente.
|
||||
|
||||
---
|
||||
|
||||
## Tranchen bilden
|
||||
|
||||
Bei größeren Beständen den Export in Blöcke teilen. Bewährt haben sich
|
||||
etwa 2.000 Dokumente je Tranche.
|
||||
|
||||
Empfohlene Struktur:
|
||||
|
||||
```
|
||||
export-daten/
|
||||
├── export1/archive/{export.data, export.xml, *.pdf, …}
|
||||
├── export2/archive/…
|
||||
└── …
|
||||
```
|
||||
|
||||
Nach ID-Bereichen teilen, nicht nach manueller Auswahl. Der Wurzelknoten
|
||||
der XML trägt `startid` und `endid` — der Exporter denkt ohnehin in
|
||||
Bereichen.
|
||||
|
||||
**Tranchen dürfen sich überschneiden.** Das Journal überspringt bereits
|
||||
importierte Dokumente. Das ist nützlich, wenn Teile des Bestands nur unter
|
||||
einem anderen Benutzerkonto sichtbar sind: einfach eine zusätzliche
|
||||
Tranche unter diesem Konto exportieren.
|
||||
|
||||
---
|
||||
|
||||
## Prüfung je Tranche
|
||||
|
||||
### 1. Abfrage plausibel?
|
||||
|
||||
Siehe oben.
|
||||
|
||||
### 2. Dokumentanzahl?
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import sqlite3
|
||||
c = sqlite3.connect('file:export.data?mode=ro', uri=True)
|
||||
for t in ('docs','klassifizierung','ecosimsversions','ecosimsarchive',
|
||||
'docrollen_hist','econotice'):
|
||||
print(f'{t:20s}', c.execute(f'SELECT COUNT(*) FROM \"{t}\"').fetchone()[0])
|
||||
"
|
||||
```
|
||||
|
||||
Sind `docs` und `klassifizierung` auf null, während `documentenart` und
|
||||
`status` gefüllt sind, hat die Abfrage nicht gegriffen.
|
||||
|
||||
### 3. Referenzen vollständig?
|
||||
|
||||
Die XML verweist auf Dateien im Archivordner. Jede fehlende Referenz ist
|
||||
ein Dokument, das beim Import scheitert:
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import xml.etree.ElementTree as ET, pathlib
|
||||
root = ET.parse('export.xml').getroot()
|
||||
ref = {e.get('filePath') for e in root.iter() if e.get('filePath')}
|
||||
have = {p.name for p in pathlib.Path('.').iterdir() if p.is_file()}
|
||||
print('fehlt:', sorted(ref - have)[:20])
|
||||
print('unreferenziert:', sorted(have - ref - {'export.xml','export.data'})[:20])
|
||||
"
|
||||
```
|
||||
|
||||
**Erwartete Fehlmeldungen:** Bei Nicht-PDF-Originalen referenziert die XML
|
||||
eine PDF/A-Fassung je Version, die nicht exportiert wird. Das ist normal
|
||||
und für Paperless unkritisch.
|
||||
|
||||
### 4. Dateitypen?
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import sqlite3, collections
|
||||
c = sqlite3.connect('file:export.data?mode=ro', uri=True)
|
||||
n = collections.Counter()
|
||||
for (f,) in c.execute('SELECT realname FROM ecosimsversions WHERE realname IS NOT NULL'):
|
||||
n['.' + f.rsplit('.',1)[-1].lower()] += 1
|
||||
for k, v in n.most_common(): print(f'{v:6d} {k}')
|
||||
"
|
||||
```
|
||||
|
||||
Vergleich mit dem, was die Zielinstanz kann:
|
||||
|
||||
```bash
|
||||
docker compose exec webserver python manage.py shell -c \
|
||||
"from documents.parsers import get_supported_file_extensions as f; print(sorted(f()))"
|
||||
```
|
||||
|
||||
Typischerweise **nicht** unterstützt: `.html`, `.htm`, `.zip`, `.indd`.
|
||||
`ecodms_extract.py` sortiert solche Dokumente aus und listet sie zur
|
||||
Nacharbeit.
|
||||
|
||||
### 5. Rollen und Rechtearten?
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import sqlite3
|
||||
c = sqlite3.connect('file:export.data?mode=ro', uri=True)
|
||||
for r in c.execute('SELECT DISTINCT role, doc_right FROM docrollen_hist ORDER BY role'):
|
||||
print(r)
|
||||
"
|
||||
```
|
||||
|
||||
Jede Rolle muss in `rollen.yml` abgebildet oder ausdrücklich verworfen
|
||||
werden. Stufe 2 bricht sonst vor dem ersten Upload ab — bewusst, denn ein
|
||||
stilles Verwerfen würde Dokumente **öffnen**, nicht schließen.
|
||||
|
||||
### 6. Mehrfachklassifizierung?
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import sqlite3, collections
|
||||
c = sqlite3.connect('file:export.data?mode=ro', uri=True)
|
||||
n = collections.Counter(d for (d,) in c.execute('SELECT docid FROM docs'))
|
||||
h = collections.Counter(n.values())
|
||||
for k in sorted(h): print(f'{h[k]:6d} Dokumente mit {k} Klassifizierung(en)')
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Was der Export nicht enthält
|
||||
|
||||
| Element | Alternative |
|
||||
|---|---|
|
||||
| Dokumentverknüpfungen | nicht ermittelbar, Nacharbeit von Hand |
|
||||
| Klassifizierungshistorie | steht in der XML, aber Paperless kann sie nicht abbilden |
|
||||
| Anhänge als eigene Dateien | reisen eingebettet im Original mit |
|
||||
|
||||
Der `lucene`-Ordner enthält den Suchindex des Offline-Clients und ist für
|
||||
die Migration wertlos.
|
||||
|
||||
---
|
||||
|
||||
## Speicherbedarf
|
||||
|
||||
Der Export enthält je Version Original und teils PDF/A. Rechne mit etwa
|
||||
dem Umfang der ecoDMS-Sicherung. Auf der Zielseite kommt Ähnliches
|
||||
hinzu, weil Paperless Original und Archivdatei getrennt ablegt.
|
||||
|
||||
---
|
||||
|
||||
Weiter mit [Extraktion und Prüfung](03-extraktion.md).
|
||||
Reference in New Issue
Block a user