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

296
docs/04-import.md Normal file
View File

@@ -0,0 +1,296 @@
# 4. Import
Stufe 2 spielt ein Manifest über die REST-API in Paperless ein und führt
dabei ein eigenes SQLite-Journal.
---
## Das Journal
`migration.sqlite` ist der Kern. Es hält je Dokument fest:
| Feld | Bedeutung |
|---|---|
| `ecodms_docid` | Quell-ID |
| `paperless_id` | Ziel-ID |
| `versions_total` / `versions_done` | Fortschritt der Kette |
| `status` | `pending`, `root_created`, `done`, `failed`, `uebersprungen` |
| `last_error` | Fehlertext im Klartext |
Daraus folgen drei Eigenschaften:
- **Wiederaufsetzbar.** Ein abgebrochener Lauf setzt fort, ohne Duplikate.
- **Tranchen dürfen sich überschneiden.** Bekannte Dokumente werden
übersprungen.
- **Tranchenübergreifend auswertbar.** Nötig für Zeitstempel und
Verknüpfungen am Ende.
Das Journal **nicht** löschen, solange die Migration läuft. Und nicht
zusammen mit dem Paperless-Bestand zurücksetzen, ohne beides gemeinsam zu
tun — sonst entstehen Doppelanlagen.
---
## Rollenübersetzung
`rollen.yml.example` nach `rollen.yml` kopieren und anpassen.
```yaml
default_owner: benutzer_a
roles:
r_benutzer_a:
type: user
paperless: benutzer_a
owner: true # wird Eigentümer, wenn beteiligt
FFN:
type: group
paperless: Gruppe_A
ecoSIMSUSER:
type: null # globales Importrecht, entfällt bewusst
rights:
R: [view]
W: [view, change]
no_rights_policy: owner_only
```
### Warum `ecoSIMSUSER` verworfen wird
Das ist das globale Importrecht — jeder hat Zugriff. Es wird beim Import
gesetzt und bei der ersten Klassifizierung ersetzt. Wer es übernimmt,
öffnet jedes Dokument für alle.
### Warum `W` beide Rechte bekommt
In Paperless implizieren `change` und `view` einander nicht. Ein `change`
ohne `view` ergibt keinen sinnvollen Zustand.
### Warum unbekannte Rollen zum Abbruch führen
**In Paperless bedeutet „keine Rechte gesetzt" nicht gesperrt, sondern
unbeschränkt.** Ein Tippfehler im Rollennamen würde Dokumente öffnen, nicht
schließen. Deshalb: entweder abbilden oder ausdrücklich als `type: null`
eintragen.
---
## Zusatzfelder vorab anlegen
```yaml
custom_fields:
Belegnummer: string
Zeitraum: string
Eingangsdatum: date
Zahlungsdatum: date
Wiedervorlage: date
ecoDMS-ID: string
ecoDMS-Verknuepfung: documentlink
```
**Nicht von Hand in der Oberfläche anlegen.** Weicht der Name auch nur
minimal ab, erzeugt das Skript ein zweites gleichnamiges Feld, und die
Werte verteilen sich auf beide.
---
## Ablauf
### Trockenlauf
```bash
export PT=dein_api_token
python3 paperless_import.py export1.json --dry-run
```
Schreibt nichts, prüft aber die Rollen gegen `rollen.yml` und meldet
Unbekanntes.
### Stammdaten
```bash
python3 paperless_import.py export1.json --setup
```
Legt Tags, Dokumenttypen und Zusatzfelder an. Vorhandene werden erkannt.
Danach in der Oberfläche ansehen: Passen die Tags? Bei tiefen
Ordnerbäumen entstehen schnell mehrere hundert. Das ist der Moment, es zu
ändern — später kostet es eine Umstellung des gesamten Bestands.
### Probelauf
```bash
python3 paperless_import.py export1.json --limit 2 --stop-on-error
```
Zwei Dokumente, Abbruch beim ersten Fehler. Danach in der Oberfläche
prüfen:
- Tags einzeln statt als Pfad?
- Archiv-Seriennummer gleich der ecoDMS-ID?
- Zusatzfelder gefüllt?
- Eigentümer gesetzt?
- Bei einem mehrversionigen Dokument: Kette vollständig?
### Volle Tranche
```bash
python3 paperless_import.py export1.json
```
Ohne `--stop-on-error`, damit einzelne Fehlschläge den Lauf nicht
anhalten. Sie landen als `failed` im Journal.
### Prüfen
```bash
python3 paperless_import.py --verify
```
Vergleicht Journal und Instanz. Meldet abweichende Versionszahlen,
fehlende Eigentümer, abweichende Seriennummern und **Dokumente ohne
Journaleintrag** — das sind mögliche Doppelanlagen.
---
## Tag-Modus
Standard ist `segments`: Aus `Beruf/Fortbildung/Steuerberater` werden drei
Tags. Das erlaubt Filtern nach einzelnen Ebenen, kostet aber den
Hierarchiekontext — `Steuerberater` allein ist mehrdeutig.
Alternative:
```bash
python3 paperless_import.py export1.json --tag-mode path
```
Ein Tag je vollständigem Pfad. Übersichtlicher, aber nicht nach Ebenen
filterbar.
**Die Entscheidung vor dem ersten `--setup` treffen.** Ein Wechsel später
bedeutet, alle Tags neu zu vergeben.
---
## Was Stufe 2 je Dokument tut
```
POST /api/documents/post_document/ → Version 1, mit Titel, created,
Dokumenttyp, Tags
Task pollen → paperless_id ins Journal
POST /api/documents/{id}/update_version/ → Version 2…n, mit version_label
Task pollen, streng seriell
PATCH /api/documents/{id}/ → Eigentümer, Rechte,
Zusatzfelder, Seriennummer
```
### Warum seriell
`update_version` hängt an den aktuellen Head an. Innerhalb einer
Versionskette muss jede Version fertig konsumiert sein, bevor die nächste
kommt. Parallelisiert wird über Dokumente hinweg, nicht innerhalb.
### Rechte nur auf die Wurzel
Versionen sind eigene Dokumentdatensätze, erben die Rechte aber über
`root_document`. Ein Zugriffstest mit einem nicht berechtigten Benutzer
liefert bei allen Versionen 403. Es genügt also, die Rechte einmal auf das
Wurzeldokument zu setzen.
### Was die API nicht kann
`update_version` nimmt ausschließlich `document` und `version_label`
entgegen — **keinen Zeitstempel**. Das `added`-Feld je Version muss
nachträglich gesetzt werden, siehe [Nacharbeit](05-nacharbeit.md).
---
## Fehler behandeln
```bash
sqlite3 -header -column migration.sqlite \
"SELECT tranche, status, COUNT(*) FROM mapping GROUP BY tranche, status;"
sqlite3 -header migration.sqlite \
"SELECT ecodms_docid, last_error FROM mapping WHERE status='failed' LIMIT 20;"
```
Nach einer Korrektur die Fehlschläge erneut anstoßen:
```bash
sqlite3 migration.sqlite \
"UPDATE mapping SET status='pending', last_error=NULL WHERE status='failed';"
python3 paperless_import.py export1.json
```
Dauerhaft nicht importierbare Dokumente auf einen eigenen Status setzen,
damit sie nicht bei jedem Lauf erneut versucht werden:
```bash
sqlite3 migration.sqlite \
"UPDATE mapping SET status='uebersprungen' WHERE ecodms_docid IN (…);"
```
### Häufige Fehlerbilder
| Meldung | Ursache |
|---|---|
| `File type … not supported` | Dateityp, oder Inhalt passt nicht zur Endung |
| `HTTP 406` bei Office/`.eml` | Tika-Version zu neu |
| `Error while queuing document` | oft eine falsche `DEFAULT_PERMISSIONS_OWNER`-ID |
| `Task … nach 600s ohne Ergebnis` | großer Scan mit OCR, `TASK_TIMEOUT` erhöhen |
| `HTTP 400` beim Anlegen eines Tags | Namenskollision nach Normalisierung |
### Zwischenzustand nach Abbruch
Wird ein Lauf zwischen Upload und Journaleintrag unterbrochen, existiert
das Dokument in Paperless ohne Journaleintrag. `--verify` findet das.
Behandlung: Zeigt das Journal `failed` und das Dokument existiert, ist der
Eintrag nachzutragen statt neu hochzuladen:
```bash
sqlite3 migration.sqlite \
"UPDATE mapping SET paperless_id=<id>, versions_done=1,
status='root_created', last_error=NULL WHERE ecodms_docid=<docid>;"
python3 paperless_import.py exportN.json
```
Der Lauf ergänzt dann Rechte, Zusatzfelder und Seriennummer.
---
## Snapshots
Vor jeder Tranche einen Snapshot des Datenverzeichnisses:
```bash
docker compose down
btrfs subvolume snapshot -r data ../snapshots/paperless-vor-tranche2
docker compose up -d
```
Container vorher stoppen, sonst ist der Datenbankstand nicht konsistent zu
den Dateien. Beim ersten Durchlauf wird man das brauchen.
---
## Laufzeit
Bei PDFs mit vorhandenem Textlayer und `OCR_MODE=auto` liegt ein
Konsumvorgang unter einer Sekunde. Für 12.000 Versionen bedeutet das
wenige Stunden.
Der Engpass ist nicht die CPU, sondern die serielle Arbeitsweise plus das
Abfrageintervall. Mehr Worker beschleunigen nichts, solange immer nur eine
Aufgabe gleichzeitig läuft.
Deutlich länger dauern Office-Dokumente und E-Mails, weil sie über
Gotenberg und Tika laufen.
---
Weiter mit [Nacharbeit](05-nacharbeit.md).