# 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=, versions_done=1, status='root_created', last_error=NULL WHERE ecodms_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).