diff --git a/database/docs/plan_pck_net_storage.md b/database/docs/plan_pck_net_storage.md index c2bee1d..b781d93 100644 --- a/database/docs/plan_pck_net_storage.md +++ b/database/docs/plan_pck_net_storage.md @@ -61,9 +61,11 @@ Alle zur Laufzeit via `pck_system.f_get_par_wert_by_programmid`: | `NETSTORE_MARKER_SB` | Name der Marker-Datei im Object Store, der von DB beim Verarbeiten angelegt wird, wenn eine oder mehrere Dateien eines ZIPs nicht automatisiert importiert werden konnten. Das soll signalisieren, dass ein Sachbearbeiter die Dateien in diesem Unterordner manuell prüfen und importieren muss. z.B.: `_BITTE_PRÜFEN_` | | `NETSTORE_BA_PREFIX` | Pfad in Object Storage, wo BA-Daten liegen. Muss mit einem `/` enden, z.B. `BA/Eingang/` | | `NETSTORE_BA_IMPORT` | Name des Unterordners von NETSTORE_BA_PREFIX im Object Storage, wo entpackte Dateien, die noch importiert werden müssen, zwischengespeichert werden. | +| `NETSTORE_BA_ARCHIV` | Der Basis-Name des Unterordners von NETSTORE_BA_PREFIX im Object Storage, wo verarbeitete BA-Imports abgelegt werden. Der Name darf nicht mit / enden oder beginnen, z.B. "Verarbeitet". Beim Import wird an diesen Namen immer die aktuelle Jahreszahl angehangen, sodass der finale Ordner z.B. "Verarbeitet 2026" heißt. | | `BA_IMPORT_SB_MIT_ID` | Mitarbeiter-ID für Import von BA Daten (z.B. Korrespondenzen). Diese Mitarbeiter-ID bekommt eine Wiedervorlage, für jede Datei, die nicht automatisch importiert werden konnte. | | `AUTOMATON_BASE_URL` | Base-URL des Quarkus Dateieingang Service, z.B. `http://dateieingang:8080` | | `AUTOMATON_API_KEY` | API-Key für den Quarkus Dateieingang Service (Header `X-Api-Key`) | +| `NETSTORE_ORDS_APIKEY` | API-Key, den der Quarkus Server nutzt, um den ORDS aufzurufen | --- diff --git a/quarkus-automaton/.gitignore b/quarkus-automaton/.gitignore index 0d95126..9af8312 100644 --- a/quarkus-automaton/.gitignore +++ b/quarkus-automaton/.gitignore @@ -1,2 +1,3 @@ .env -target \ No newline at end of file +target +*private-key.pem \ No newline at end of file diff --git a/quarkus-automaton/src/main/java/de/galabau/dateieingang/config/OciConfig.java b/quarkus-automaton/src/main/java/de/galabau/dateieingang/config/OciConfig.java new file mode 100644 index 0000000..18b1519 --- /dev/null +++ b/quarkus-automaton/src/main/java/de/galabau/dateieingang/config/OciConfig.java @@ -0,0 +1,45 @@ +package de.galabau.dateieingang.config; + +import io.smallrye.config.ConfigMapping; + +/** OCI Object Storage Konfiguration. Credentials kommen ausschließlich aus Umgebungsvariablen. */ +@ConfigMapping(prefix = "galabau.oci") +public interface OciConfig { + + /** OCI Object Storage Namespace, z.B. {@code frhqaxi5sgcg}. */ + String namespace(); + + /** OCI Region, z.B. {@code eu-frankfurt-1}. */ + String region(); + + /** OCI Bucket-Name. */ + String bucket(); + + /** + * Root-Prefix für alle Objekte im Bucket, z.B. {@code mandant_42/}. + * Muss mit {@code /} enden. + */ + String tenantPrefix(); + + /** + * Prefix für eingehende Dateien unterhalb von {@code tenantPrefix}, + * z.B. {@code eingang/}. Muss mit {@code /} enden. + */ + String incomingPrefix(); + + /** OCI Tenancy OCID. Aus Env-Var {@code OCI_TENANCY_ID}. */ + String tenancyId(); + + /** OCI User OCID. Aus Env-Var {@code OCI_USER_ID}. */ + String userId(); + + /** API Key Fingerprint, z.B. {@code aa:bb:cc:...}. Aus Env-Var {@code OCI_FINGERPRINT}. */ + String fingerprint(); + + /** + * Dateisystempfad zur PEM-Datei des OCI API Keys. + * Produktion: absoluter Pfad zum Kubernetes Secret Volume Mount. + * Dev: relativer Pfad zum Projektverzeichnis (Default: {@code oci-private-key.pem}). + */ + String privateKeyPath(); +} diff --git a/quarkus-automaton/src/main/java/de/galabau/dateieingang/config/OrdsConfig.java b/quarkus-automaton/src/main/java/de/galabau/dateieingang/config/OrdsConfig.java new file mode 100644 index 0000000..b947c23 --- /dev/null +++ b/quarkus-automaton/src/main/java/de/galabau/dateieingang/config/OrdsConfig.java @@ -0,0 +1,21 @@ +package de.galabau.dateieingang.config; + +import io.smallrye.config.ConfigMapping; + +/** ORDS-Konfiguration für den Dateieingang-Endpunkt. */ +@ConfigMapping(prefix = "galabau.ords") +public interface OrdsConfig { + + /** + * Base-URL des ORDS-Moduls bis einschließlich Modul-Pfad, + * z.B. {@code https://apex.example.com/ords/myschema/auto_import}. + * Wird direkt als {@code quarkus.rest-client.ords-client.url} verwendet. + */ + String baseUrl(); + + /** + * API-Key für den ORDS-Endpunkt (Header: {@code X-Api-Key}). + * Aus Env-Var {@code GALABAU_ORDS_API_KEY}. + */ + String apiKey(); +} diff --git a/quarkus-automaton/src/main/java/de/galabau/dateieingang/oci/OciUploadService.java b/quarkus-automaton/src/main/java/de/galabau/dateieingang/oci/OciUploadService.java index cffdac8..189f288 100644 --- a/quarkus-automaton/src/main/java/de/galabau/dateieingang/oci/OciUploadService.java +++ b/quarkus-automaton/src/main/java/de/galabau/dateieingang/oci/OciUploadService.java @@ -1,32 +1,121 @@ package de.galabau.dateieingang.oci; +import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider; +import com.oracle.bmc.objectstorage.ObjectStorage; +import com.oracle.bmc.objectstorage.ObjectStorageClient; +import com.oracle.bmc.objectstorage.requests.PutObjectRequest; +import de.galabau.dateieingang.config.OciConfig; import de.galabau.dateieingang.exception.OciException; +import de.galabau.dateieingang.model.FileEntry; import de.galabau.dateieingang.model.ProcessingContext; import de.galabau.dateieingang.model.ProcessingStatus; import io.quarkus.logging.Log; +import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; /** * Lädt die entpackten Dateien und den Marker in OCI Object Storage hoch. - * - *
Stub: OCI-Upload ist noch nicht implementiert.
- * Der Upload wird übersprungen und der Status auf {@link ProcessingStatus#MARKER_UPLOADED} gesetzt,
- * damit der Rest der Pipeline (SFTP-Rename, ORDS-Notify) getestet werden kann.
+ * Authentifizierung via OCI HTTP Signature V1 (entspricht APEX Web Credential vom Typ OCI).
*/
@ApplicationScoped
public class OciUploadService {
+ @Inject
+ OciConfig config;
+
+ private ObjectStorage client;
+
+ @PostConstruct
+ void init() {
+ SimpleAuthenticationDetailsProvider auth = SimpleAuthenticationDetailsProvider.builder()
+ .tenantId(config.tenancyId())
+ .userId(config.userId())
+ .fingerprint(config.fingerprint())
+ .region(com.oracle.bmc.Region.fromRegionId(config.region()))
+ .privateKeySupplier(() -> {
+ try {
+ return Files.newInputStream(Path.of(config.privateKeyPath()));
+ } catch (IOException e) {
+ throw new RuntimeException("OCI Private Key nicht lesbar: "
+ + config.privateKeyPath(), e);
+ }
+ })
+ .build();
+
+ this.client = ObjectStorageClient.builder().build(auth);
+ }
+
/**
* Lädt alle Dateien aus {@code context.extractedFiles} sowie den Marker in OCI hoch.
+ * Dateien mit {@code isMarker = true} werden übersprungen — der Marker wird separat
+ * am Ende hochgeladen, um sicherzustellen dass er erst nach allen Dateien erscheint.
*
* @param context enthält die Liste der hochzuladenden Dateien und den Ziel-Prefix
- * @throws OciException bei persistenten OCI-Fehlern (4xx) nach Retry-Erschöpfung
+ * @throws OciException bei Verbindungs- oder Upload-Fehlern
*/
public void upload(ProcessingContext context) throws OciException {
- // TODO: OCI-Upload implementieren (OCI SDK, SimpleAuthenticationDetailsProvider)
- Log.infof("[STUB] OCI-Upload übersprungen für '%s' (%d Dateien) — wird später implementiert",
- context.zipNameWithoutExt, context.extractedFiles.size());
+ context.status = ProcessingStatus.PARTIALLY_UPLOADED;
+
+ List Stub: ORDS-Aufruf ist noch nicht implementiert.
- * Bei einem Ausfall wäre die Verarbeitung ohnehin durch die APEX Automation abgesichert
- * (diese findet den Marker beim nächsten Stundenlauf).
+ * Bei Ausfall ist die Verarbeitung durch die APEX Automation abgesichert:
+ * Sie findet den Marker beim nächsten Stundenlauf und ruft die Prozedur selbst auf.
*/
@ApplicationScoped
public class OrdsNotificationService {
+ @Inject
+ @RestClient
+ OrdsClient ordsClient;
+
+ @Inject
+ OrdsConfig config;
+
/**
* Sendet eine Benachrichtigung an den ORDS-Endpunkt.
+ * Wird bei transienten Fehlern bis zu 3-mal wiederholt (1s Backoff).
*
- * @param context enthält {@code zipNameWithoutExt} und {@code runId} für den Request-Body
+ * @param context enthält {@code zipNameWithoutExt} für das Log
* @throws OrdsException wenn der ORDS-Aufruf nach allen Retries fehlschlägt
*/
+ @Retry(maxRetries = 3, delay = 1000, delayUnit = ChronoUnit.MILLIS,
+ retryOn = OrdsException.class)
+ @Timeout(value = 10, unit = ChronoUnit.SECONDS)
public void notify(ProcessingContext context) throws OrdsException {
- // TODO: ORDS REST-Client implementieren (MicroProfile REST Client + @Retry)
- Log.infof("[STUB] ORDS-Benachrichtigung übersprungen für '%s' — wird später implementiert",
- context.zipNameWithoutExt);
+ Response response;
+ try {
+ response = ordsClient.processIncomingBaData(config.apiKey());
+ } catch (Exception e) {
+ throw new OrdsException("ORDS-Verbindung fehlgeschlagen für '"
+ + context.zipNameWithoutExt + "'", e);
+ }
+
+ int status = response.getStatus();
+ if (status >= 400) {
+ throw new OrdsException("ORDS antwortete mit HTTP " + status
+ + " für '" + context.zipNameWithoutExt + "'");
+ }
+
+ Log.infof("ORDS-Endpunkt aufgerufen, HTTP %d für '%s'", status, context.zipNameWithoutExt);
}
}
diff --git a/quarkus-automaton/src/main/java/de/galabau/dateieingang/pipeline/FileProcessingPipeline.java b/quarkus-automaton/src/main/java/de/galabau/dateieingang/pipeline/FileProcessingPipeline.java
index d2e6e37..6376f5f 100644
--- a/quarkus-automaton/src/main/java/de/galabau/dateieingang/pipeline/FileProcessingPipeline.java
+++ b/quarkus-automaton/src/main/java/de/galabau/dateieingang/pipeline/FileProcessingPipeline.java
@@ -113,7 +113,7 @@ public class FileProcessingPipeline {
Log.infof("ZIP '%s' entpackt: %d Datei(en)", zipFilename,
context.extractedFiles.size());
- // --- OCI Upload (Stub) ---
+ // --- OCI Upload ---
MDC.put("step", "oci-upload");
ociUploadService.upload(context);
@@ -122,7 +122,7 @@ public class FileProcessingPipeline {
sftpService.renameRemote(zipFilename, zipFilename + ".processed");
Log.infof("SFTP Rename: '%s' → '%s.processed'", zipFilename, zipFilename);
- // --- ORDS Notify (Stub) ---
+ // --- ORDS Notify ---
MDC.put("step", "ords-notify");
ordsNotificationService.notify(context);
diff --git a/quarkus-automaton/src/main/java/de/galabau/dateieingang/sftp/SftpService.java b/quarkus-automaton/src/main/java/de/galabau/dateieingang/sftp/SftpService.java
index 120553d..42db230 100644
--- a/quarkus-automaton/src/main/java/de/galabau/dateieingang/sftp/SftpService.java
+++ b/quarkus-automaton/src/main/java/de/galabau/dateieingang/sftp/SftpService.java
@@ -60,8 +60,10 @@ public class SftpService {
if (config.hostKeyFingerprint().isPresent()) {
ssh.addHostKeyVerifier(config.hostKeyFingerprint().get());
} else {
- Log.warn("SFTP Host-Key-Fingerprint nicht konfiguriert — PromiscuousVerifier aktiv (nur Dev!)");
- ssh.addHostKeyVerifier(new PromiscuousVerifier());
+ Log.warn("SFTP Host-Key-Fingerprint nicht konfiguriert");
+ throw new IllegalStateException(
+ "SFTP Host-Key-Fingerprint muss konfiguriert sein!"
+ );
}
}
diff --git a/quarkus-automaton/src/main/resources/application.properties b/quarkus-automaton/src/main/resources/application.properties
index b4ce63d..2b9db08 100644
--- a/quarkus-automaton/src/main/resources/application.properties
+++ b/quarkus-automaton/src/main/resources/application.properties
@@ -18,22 +18,26 @@ 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 (Stub — noch nicht aktiv) =====
-# galabau.oci.namespace=${OCI_NAMESPACE}
-# galabau.oci.region=${OCI_REGION}
-# galabau.oci.bucket=${OCI_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}
+# ===== OCI Object Storage =====
+galabau.oci.namespace=${OCI_NAMESPACE}
+galabau.oci.region=${OCI_REGION}
+galabau.oci.bucket=${OCI_BUCKET}
+# Root-Prefix im Bucket, muss mit / enden
+galabau.oci.tenant-prefix=${OCI_TENANT_PREFIX:testmandant-42/}
+# Eingangs-Prefix unterhalb von tenant-prefix, muss mit / enden
+galabau.oci.incoming-prefix=${OCI_INCOMING_FILES_PATH:BA/Eingang/Import/}
+galabau.oci.tenancy-id=${OCI_TENANCY_ID}
+galabau.oci.user-id=${OCI_USER_ID}
+galabau.oci.fingerprint=${OCI_FINGERPRINT}
+%prod.galabau.oci.private-key-path=${OCI_PRIVATE_KEY_PATH}
+%dev.galabau.oci.private-key-path=${OCI_PRIVATE_KEY_PATH:oci-private-key.pem}
-# ===== ORDS (Stub — noch nicht aktiv) =====
-# galabau.ords.base-url=${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}
+# ===== ORDS =====
+# Base-URL bis einschließlich Modul-Pfad, z.B. https://apex.example.com/ords/myschema/auto_import
+galabau.ords.base-url=${GALABAU_ORDS_BASE_URL}
+galabau.ords.api-key=${GALABAU_ORDS_API_KEY}
+# MicroProfile REST Client liest die URL aus dieser Property:
+quarkus.rest-client.ords-client.url=${galabau.ords.base-url}
# ===== Observability =====
%prod.quarkus.otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317}