Initial commit

This commit is contained in:
2026-09-04 19:51:02 +02:00
parent 7840783dc6
commit 13bae7d600
14 changed files with 3766 additions and 0 deletions

View 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).