# Architektur — Dateieingang Service ## Überblick Der Service empfängt einen HTTP-Trigger von der APEX Automation, verbindet sich mit einem SFTP-Server, lädt neue ZIP-Dateien herunter, entpackt sie und lädt den Inhalt in OCI Object Storage hoch. Anschließend wird der ORDS-Endpunkt `pck_auto_import.p_process_incoming_ba_data` aufgerufen, der die DB-Verarbeitung anstößt. Das Scheduling bleibt bei APEX — der Service selbst hat keinen eigenen Scheduler. ## Datenfluss ``` APEX Automation (stündlich) │ ▼ HTTP POST /api/process-incoming (Header: X-Api-Key) FileProcessingResource │ 202 Accepted (sofort) ▼ FileProcessingPipeline [ManagedExecutor — Hintergrund-Thread] │ │ für jede *.zip auf dem SFTP-Server: │ ├─→ SftpService.listZipFiles() [SSHJ] ├─→ SftpService.download() [SSHJ] │ ├─→ ZipExtractionService.extract() [Apache Commons Compress] │ └─ ProcessingContext mit List │ ├─→ OciUploadService.upload() [OCI SDK] │ └─ Dateien in eingang// + Marker │ ├─→ SftpService.renameRemote() [SSHJ] │ └─ .processed (Erfolg) oder .error (Fehler) │ ├─→ OrdsNotificationService.notify() [MicroProfile REST Client] │ └─ POST pck_auto_import.p_process_incoming_ba_data │ └─→ Cleanup: lokale Dateien löschen [immer, im finally] │ ▼ Oracle DB (pck_auto_import verarbeitet eingang//) ``` ## Pipeline-Steps | Step-Name | Klasse | Paket | Bibliothek | |---|---|---|---| | `api-trigger` | `FileProcessingResource` | `api` | Quarkus REST | | `sftp-list` | `SftpService` | `sftp` | SSHJ | | `sftp-download` | `SftpService` | `sftp` | SSHJ | | `zip-extract` | `ZipExtractionService` | `zip` | Apache Commons Compress | | `oci-upload` | `OciUploadService` | `oci` | OCI Java SDK | | `sftp-rename` | `SftpService` | `sftp` | SSHJ | | `ords-notify` | `OrdsNotificationService` | `ords` | MicroProfile REST Client | | `cleanup` | `FileProcessingPipeline` | `pipeline` | pure Java | ## Orchestrierung `FileProcessingPipeline` ist der einzige Orchestrierer — kein Framework-Routing. Die Pipeline läuft in einem `ManagedExecutor`-Thread (Quarkus Context Propagation), damit MDC-Kontext und CDI-Scope im Hintergrund-Thread verfügbar bleiben. ``` FileProcessingResource [REST-Thread] │ 202 sofort ▼ FileProcessingPipeline [ManagedExecutor-Thread] │ AtomicBoolean verhindert Doppelläufe │ └─ processAll() └─ für jede ZIP: try { ... } finally { cleanup(); MDC.clear(); } ``` Der `AtomicBoolean isRunning`-Guard stellt sicher, dass ein zweiter APEX-Trigger während eines laufenden Durchgangs mit `409 Conflict` abgewiesen wird, statt einen zweiten Lauf zu starten. ## Konfiguration (`application.properties`) ```properties # API-Absicherung galabau.api.key=${GALABAU_API_KEY} # SFTP galabau.sftp.host=sftp.lieferant.de galabau.sftp.port=22 galabau.sftp.username=sftp_user galabau.sftp.password=${GALABAU_SFTP_PASSWORD} galabau.sftp.host-key-fingerprint=SHA256:... # ssh-keyscan host | ssh-keygen -lf - galabau.sftp.remote-path=/outgoing galabau.sftp.local-work-dir=/tmp/sftp-work # galabau.sftp.private-key-path=/etc/secrets/sftp-key # galabau.sftp.private-key-passphrase=${SFTP_KEY_PASSPHRASE} # OCI Object Storage galabau.oci.namespace=mycompany galabau.oci.region=eu-frankfurt-1 galabau.oci.bucket=my-bucket galabau.oci.tenant-prefix=mandant_42/ galabau.oci.incoming-prefix=eingang/ galabau.oci.tenancy-id=${OCI_TENANCY_ID} galabau.oci.user-id=${OCI_USER_ID} galabau.oci.fingerprint=${OCI_FINGERPRINT} galabau.oci.private-key-path=${OCI_PRIVATE_KEY_PATH} # ORDS galabau.ords.base-url=http://ords:8080 galabau.ords.process-incoming-path=/ords/.../net_storage/process_incoming_ba_data galabau.ords.api-key=${GALABAU_ORDS_API_KEY} quarkus.rest-client.ords-client.url=${galabau.ords.base-url} # Retry galabau.retry.max-attempts=3 galabau.retry.backoff-ms=1000 ``` ## Fehlerbehandlung | Fehler | Typ | Handling | |---|---|---| | SFTP-Verbindung fehlgeschlagen | transient | Pipeline bricht ab, nächster APEX-Lauf (1h) | | ZIP beschädigt | persistent | SFTP-Rename zu `.error`, Log, weiter mit nächster ZIP | | OCI 503 | transient | `@Retry` (3x mit Backoff) | | OCI 403 Auth Failed | persistent | SFTP-Rename zu `.error`, Log — Credentials prüfen! | | ORDS-Aufruf fehlgeschlagen | transient | Marker liegt vor, APEX findet ihn nächsten Lauf | ## OCI-Authentifizierung `SimpleAuthenticationDetailsProvider` mit Credentials aus Umgebungsvariablen (skalare Werte) und gemounteter PEM-Datei (Private Key via Kubernetes Secret Volume). ```java SimpleAuthenticationDetailsProvider auth = SimpleAuthenticationDetailsProvider.builder() .tenantId(config.tenancyId()) .userId(config.userId()) .fingerprint(config.fingerprint()) .region(Region.fromRegionId(config.region())) .privateKeySupplier(new FilePrivateKeySupplier(config.privateKeyPath())) .build(); ``` ## Datenmodell ### `ProcessingContext` | Feld | Typ | Beschreibung | |---|---|---| | `runId` | `UUID` | Eindeutige ID dieses Verarbeitungslaufs (MDC-Kontext) | | `zipFilename` | `String` | z.B. `export_2026-04-08.zip` | | `zipNameWithoutExt` | `String` | z.B. `export_2026-04-08` | | `localZipPath` | `Path` | Lokale ZIP-Datei (für Cleanup) | | `localExtractDir` | `Path` | Lokales Entpack-Verzeichnis (für Cleanup) | | `extractedFiles` | `List` | Liste der entpackten Dateien | | `markerUploaded` | `boolean` | Marker in OCI hochgeladen? | | `startTime` | `LocalDateTime` | Startzeitpunkt | | `status` | `ProcessingStatus` | `PENDING / PARTIALLY_UPLOADED / MARKER_UPLOADED / ORDS_NOTIFIED / FAILED` | ### `FileEntry` | Feld | Typ | Beschreibung | |---|---|---| | `relativePath` | `String` | z.B. `subdir/file.csv` | | `ociKey` | `String` | z.B. `eingang/export_2026-04-08/subdir/file.csv` | | `fileSize` | `long` | Dateigröße in Bytes | | `isMarker` | `boolean` | `true` nur für `_READY_FOR_DB_PROCESSING_` |