167 lines
6.1 KiB
Markdown
167 lines
6.1 KiB
Markdown
|
|
# 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_files`
|
||
|
|
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<FileEntry>
|
||
|
|
│
|
||
|
|
├─→ OciUploadService.upload() [OCI SDK]
|
||
|
|
│ └─ Dateien in eingang/<zip-name>/ + Marker
|
||
|
|
│
|
||
|
|
├─→ SftpService.renameRemote() [SSHJ]
|
||
|
|
│ └─ .processed (Erfolg) oder .error (Fehler)
|
||
|
|
│
|
||
|
|
├─→ OrdsNotificationService.notify() [MicroProfile REST Client]
|
||
|
|
│ └─ POST pck_auto_import.p_process_incoming_files
|
||
|
|
│
|
||
|
|
└─→ Cleanup: lokale Dateien löschen [immer, im finally]
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
Oracle DB (pck_auto_import verarbeitet eingang/<zip-name>/)
|
||
|
|
```
|
||
|
|
|
||
|
|
## 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/.../auto_import/process_incoming
|
||
|
|
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<FileEntry>` | 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_` |
|