Compare commits

...

27 Commits

Author SHA1 Message Date
c74e49e11b Upload der BA ZIP Dateien hinzugefügt 2026-05-13 16:17:03 +02:00
a099d28113 zertifikatsfix in netstorage 2026-05-07 10:21:54 +02:00
a2471befe2 logging bei netzwerkfehler verbessert 2026-05-07 08:50:47 +02:00
e70a9176ca Testsckript für https Verbindung (insbes. Zertifikate) hinzugefügt 2026-05-04 10:41:27 +02:00
6418e14be3 Dokumentation upgedatet für letzte code changes 2026-05-04 10:18:36 +02:00
ca7b26509a Encoding der DB Dateien zu Windows 1252 konvertiert 2026-05-04 10:15:18 +02:00
a74b220ed7 runId in fileId für Logging umbenannt, neue pipelineRunId eingeführt und ORDS aufruf einmalig ans Ende der Pipeline gesetzt, anstatt nach jeder Datei 2026-05-04 08:41:03 +02:00
99f492d5eb Kleine Optimierungen von Logmessages 2026-05-04 08:39:19 +02:00
30170d85ac automaton angepasst, sodass er erfolgreich hochgeladene Dateien vom SFTP löscht 2026-04-30 16:12:46 +02:00
b17ce20ee0 Parameternamen geändert für BA Korrespondenzen Ordner 2026-04-30 15:30:38 +02:00
051c10dfad Config Wert für BA Korrespondenzen Import Folder umbenannt 2026-04-30 15:28:41 +02:00
ba4b28cf44 Prozedur zum erstellen von Wiedervorlage in error-fall (autonomous transaction) hinzugefügt 2026-04-27 16:52:56 +02:00
f40a40e622 log für fehlerhafte API Keys hinzugefügt 2026-04-27 16:52:19 +02:00
485211b169 Nicht benötigten import auskommentiert 2026-04-23 15:28:41 +02:00
93191ec65d put_object für leere und nicht leere dateien getestet und korrigiert 2026-04-23 15:27:52 +02:00
5098dd38a5 Mehr logging in DB packages hinzugefügt 2026-04-23 12:19:30 +02:00
838f6e96e0 Dateien für docker image build & push angelegt 2026-04-23 10:37:52 +02:00
b159bdd351 Dauer auf Millisekunden umgestellt 2026-04-22 16:20:05 +02:00
d36b346f98 Logging für ORDS Request s& Responses hinzugefügt 2026-04-22 15:54:53 +02:00
1c303f1376 OCI object storage api fehler behoben 2026-04-22 15:25:24 +02:00
aa0ed5d763 Error handling verbessert und OCI Verbindungsaufbau Problem behoben 2026-04-22 14:04:46 +02:00
e7fb09069c Zusätzliche logs eingefügt 2026-04-22 13:07:01 +02:00
f7a9113a57 Parameter NETSTORE_BA_ARCHIV hinzugefügt 2026-04-22 11:35:45 +02:00
9a445288f8 Logging und refactoring verbesserungen 2026-04-22 10:07:14 +02:00
cbcc6922a4 OCI / ORDS Implementierung hinzugefügt 2026-04-22 08:55:54 +02:00
599912ef94 Refactoring von Configs in zentralen config Ordner 2026-04-21 15:13:57 +02:00
8f7fd949f4 Automaton endpunkt constante angelegt 2026-04-21 15:13:39 +02:00
33 changed files with 1179 additions and 337 deletions

View File

@@ -7,7 +7,17 @@
"Bash(sed -n '465,478p' \"C:\\\\src\\\\Galabau\\\\glb-spielwiese\\\\database\\\\packages\\\\pck_net_storage.pkb\")", "Bash(sed -n '465,478p' \"C:\\\\src\\\\Galabau\\\\glb-spielwiese\\\\database\\\\packages\\\\pck_net_storage.pkb\")",
"Bash(sed -n '523,535p' \"C:\\\\src\\\\Galabau\\\\glb-spielwiese\\\\database\\\\packages\\\\pck_net_storage.pkb\")", "Bash(sed -n '523,535p' \"C:\\\\src\\\\Galabau\\\\glb-spielwiese\\\\database\\\\packages\\\\pck_net_storage.pkb\")",
"Bash(sed -n '582,600p' \"C:\\\\src\\\\Galabau\\\\glb-spielwiese\\\\database\\\\packages\\\\pck_net_storage.pkb\")", "Bash(sed -n '582,600p' \"C:\\\\src\\\\Galabau\\\\glb-spielwiese\\\\database\\\\packages\\\\pck_net_storage.pkb\")",
"WebFetch(domain:docs.public.oneportal.content.oci.oraclecloud.com)" "WebFetch(domain:docs.public.oneportal.content.oci.oraclecloud.com)",
"Bash(./mvnw compile *)",
"WebFetch(domain:medium.com)",
"WebFetch(domain:quarkus.io)",
"WebFetch(domain:github.com)",
"WebFetch(domain:walidhajeri.hashnode.dev)",
"Bash(find \"C:\\\\\\\\src\\\\\\\\Galabau\\\\\\\\glb-spielwiese\\\\\\\\automaton\" -name \"FileProcessingPipeline.java\")",
"PowerShell(Get-ChildItem -Path \"C:\\\\src\\\\Galabau\\\\glb-spielwiese\\\\quarkus-automaton\\\\src\\\\main\\\\java\" -Recurse | Where-Object { !$_.PSIsContainer } | Select-Object FullName)",
"PowerShell(cmd /c \"dir /s /b C:\\\\src\\\\Galabau\\\\glb-spielwiese\\\\quarkus-automaton\\\\src\\\\main\\\\java\")",
"Bash(Get-ChildItem -Path \"C:\\\\src\\\\Galabau\\\\glb-spielwiese\" -Directory)",
"Bash(Select-Object Name)"
] ]
} }
} }

View File

@@ -60,10 +60,12 @@ Alle zur Laufzeit via `pck_system.f_get_par_wert_by_programmid`:
| `NETSTORE_MARKER_DB` | Name der Marker-Datei im Object Store, der von Automaton abgelegt wird, um zu signalisieren, dass der entsprechende Unterordner komplett hochgeladen wurde. Verhindert die Verarbeitung von unvollständig hochgeladenen Ordnern. z.B.: `_READY_FOR_DB_PROCESSING_` | | `NETSTORE_MARKER_DB` | Name der Marker-Datei im Object Store, der von Automaton abgelegt wird, um zu signalisieren, dass der entsprechende Unterordner komplett hochgeladen wurde. Verhindert die Verarbeitung von unvollständig hochgeladenen Ordnern. z.B.: `_READY_FOR_DB_PROCESSING_` |
| `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_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_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_KOR_IM` | Name des Unterordners von NETSTORE_BA_PREFIX im Object Storage, wo entpackte Dateien, die noch importiert werden müssen, zwischengespeichert werden. z.B. `Import/BA-Korrespondenzen/` |
| `NETSTORE_BA_KOR_ARC` | 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. `BA-Korrespondenzen`. Beim Import wird an diesen Namen immer die aktuelle Jahreszahl angehangen, sodass der finale Ordner z.B. `BA-Korrespondenzen 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. | | `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_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`) | | `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 |
--- ---

View File

@@ -2,18 +2,39 @@ create or replace package body pck_auto_import as
c_log_module constant lg_app_log.log_module%type := 'AUTOMATISCHER_BA_IMPORT'; c_log_module constant lg_app_log.log_module%type := 'AUTOMATISCHER_BA_IMPORT';
PROCEDURE p_create_wv_autonomous(i_swv_bemerkung inkasso.sy_wiedervorlage.swv_bemerkung%TYPE DEFAULT NULL
)
/*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Erstellt eine Wiedervorlage innerhalb eine autonomous transaction, damit rollback/commits in Fehlerfällen, diese Wiedervorlage nicht beeinflussen.
------------------------------------------------------------------------------------------------------
-- Parameter: —
------------------------------------------------------------------------------------------------------
-- MA Datum Änderung
-- SCK 2026-04-27 Prozedur erstellt
------------------------------------------------------------------------------------------------Kopf*/
is
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
pck_wiedervorlage.p_wiedervorlage_anlegen (i_swv_stp_id_art => pck_stammdaten.f_get_stp_id_by_programmid('WV_IMPORT_BA_KORRES')
,i_swv_wdatum => sysdate
,i_swv_bemerkung => i_swv_bemerkung --'Bitte manuell Prüfen: Beim automatischen Import der BA-Datei "' || l_filename || '" ist folgende Fehler aufgetreten: "' || SQLERRM || '" (Siehe "' || i_object_key || '").'
,i_swv_mit_id_wsachbearbeiter => pck_system.f_get_par_wert_by_programmid('BA_IMPORT_SB_MIT_ID')
);
COMMIT; -- Nur diese autonome Transaktion
END;
procedure p_run_ba_korrespondenz_dateieingang_automation procedure p_run_ba_korrespondenz_dateieingang_automation
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Einstiegspunkt für die APEX Automation (stündlich). -- Beschreibung: Einstiegspunkt für die APEX Automation (stündlich).
-- Schritt 1: p_process_incoming_ba_data aufrufen — verarbeitet Batches, die bereits -- Schritt 1: p_process_incoming_ba_data aufrufen — verarbeitet Batches, die bereits
-- in OCI liegen (Fallback für den Fall, dass der ORDS-Aufruf im letzten -- in OCI liegen (Fallback für den Fall, dass der ORDS-Aufruf im letzten
-- Quarkus-Lauf fehlgeschlagen ist). -- Quarkus-Lauf fehlgeschlagen ist).
-- Schritt 2: Quarkus Dateieingang Service via HTTP POST anstoßen (fire & forget). -- Schritt 2: Quarkus Dateieingang Service via HTTP POST anstoßen (fire & forget).
-- Schlägt Schritt 2 fehl, läuft Schritt 1 beim nächsten Stundenlauf erneut. -- Schlägt Schritt 2 fehl, läuft Schritt 1 beim nächsten Stundenlauf erneut.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: — -- Parameter: —
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-21 Prozedur erstellt -- SCK 2026-04-21 Prozedur erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -22,22 +43,48 @@ create or replace package body pck_auto_import as
l_response clob; l_response clob;
l_http_status number; l_http_status number;
l_log_action constant varchar2(512 char) := 'BA_KORRESPONDENZEN_DATEIEINGANG_AUTOMATION'; l_log_action constant varchar2(512 char) := 'BA_KORRESPONDENZEN_DATEIEINGANG_AUTOMATION';
l_automaton_endpoint constant varchar2(256 char) := 'api/process-incoming-ba-korrespondenz';
begin begin
-- Schritt 1: Offene Batches in OCI verarbeiten pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'Automation gestartet'
);
-- Offene Batches in OCI verarbeiten
pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'Verarbeitung offener BA-Korrespondenz-Dateien aus OCI gestartet'
);
p_process_incoming_ba_data; p_process_incoming_ba_data;
-- Schritt 2: Quarkus anstoßen — Fehler werden geloggt, nicht eskaliert pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'Verarbeitung offener BA-Korrespondenz-Dateien aus OCI abgeschlossen'
);
-- Quarkus anstoßen — Fehler werden geloggt, nicht eskaliert
pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'Quarkus Dateieingang Service wird angestoßen'
);
begin begin
l_service_url := pck_system.f_get_par_wert_by_programmid('AUTOMATON_BASE_URL') || '/api/process-incoming'; l_service_url := pck_system.f_get_par_wert_by_programmid('AUTOMATON_BASE_URL') || l_automaton_endpoint;
l_api_key := pck_system.f_get_par_wert_by_programmid('AUTOMATON_API_KEY'); l_api_key := pck_system.f_get_par_wert_by_programmid('AUTOMATON_API_KEY');
apex_web_service.g_request_headers.delete; apex_web_service.g_request_headers.delete;
apex_web_service.g_request_headers(1).name := 'X-Api-Key'; apex_web_service.g_request_headers(1).name := 'X-Api-Key';
--dbms_output.put_line(l_api_key);
apex_web_service.g_request_headers(1).value := l_api_key; apex_web_service.g_request_headers(1).value := l_api_key;
l_response := apex_web_service.make_rest_request( l_response := apex_web_service.make_rest_request(
p_url => l_service_url p_url => l_service_url
,p_http_method => 'POST' ,p_http_method => 'POST'
,p_wallet_path => pck_system.f_get_par_wert_by_programmid('NETSTORE_WALLET_PATH')
); );
l_http_status := apex_web_service.g_status_code; l_http_status := apex_web_service.g_status_code;
@@ -47,33 +94,34 @@ create or replace package body pck_auto_import as
pck_log.p_info( pck_log.p_info(
i_module => c_log_module i_module => c_log_module
,i_action => l_log_action ,i_action => l_log_action
,i_message => 'Dateieingang Service angestoßen (202 Accepted)' ,i_message => 'BA Dateieingang Service angestoßen (202 Accepted)'
); );
when 409 when 409
then then
-- Service läuft bereits — kein Fehler, kein zweiter Lauf nötig -- Service läuft bereits — kein Fehler, kein zweiter Lauf nötig
pck_log.p_info( pck_log.p_info(
i_module => c_log_module i_module => c_log_module
,i_action => l_log_action ,i_action => l_log_action
,i_message => 'Dateieingang Service läuft bereits (409 Conflict) — kein neuer Lauf gestartet' ,i_message => 'BA Dateieingang Service läuft bereits (409 Conflict) — kein neuer Lauf gestartet'
); );
else else
pck_log.p_warn( pck_log.p_error(
i_module => c_log_module i_module => c_log_module
,i_action => l_log_action ,i_action => l_log_action
,i_message => 'Dateieingang Service: unerwarteter HTTP-Status ' || l_http_status ,i_message => 'Fehler in BA Dateieingang Service: unerwarteter HTTP-Status ' || l_http_status
,i_detail => l_response
); );
end case; end case;
exception exception
when others when others
then then
-- Quarkus-Aufruf fehlgeschlagen: loggen, nicht eskalieren. -- Quarkus-Aufruf fehlgeschlagen: loggen, nicht eskalieren.
-- Nächster Stundenlauf führt Schritt 1 erneut aus. -- Nächster Stundenlauf führt BA-Import-Schritt erneut aus.
pck_log.p_error( pck_log.p_error(
i_module => c_log_module i_module => c_log_module
,i_action => l_log_action ,i_action => l_log_action
,i_message => 'Aufruf des Dateieingang Service fehlgeschlagen: ' || sqlerrm ,i_message => 'Aufruf des BA Dateieingang Service fehlgeschlagen: ' || sqlerrm
,i_detail => to_clob(dbms_utility.format_error_backtrace) ,i_detail => to_clob(dbms_utility.format_error_stack) || ' -- Backtrace: -- ' || to_clob(dbms_utility.format_error_backtrace)
); );
end; end;
end p_run_ba_korrespondenz_dateieingang_automation; end p_run_ba_korrespondenz_dateieingang_automation;
@@ -86,16 +134,16 @@ create or replace package body pck_auto_import as
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Importiert eine einzelne Datei aus dem OCI Eingangsordner in die Datenbank. -- Beschreibung: Importiert eine einzelne Datei aus dem OCI Eingangsordner in die Datenbank.
-- Ruft inkasso.pck_import.f_import_ba_dokument auf. -- Ruft inkasso.pck_import.f_import_ba_dokument auf.
-- Bei Rückgabe 1 (Erfolg): Datei in den Zielordner verschieben. -- Bei Rückgabe 1 (Erfolg): Datei in den Zielordner verschieben.
-- Bei Rückgabe != 1 (z.B. ungültiger Dateiname): Warnung loggen und Exception werfen -- Bei Rückgabe != 1 (z.B. ungültiger Dateiname): Warnung loggen und Exception werfen
-- — Datei bleibt liegen, Commit/Rollback liegt beim Aufrufer. -- — Datei bleibt liegen, Commit/Rollback liegt beim Aufrufer.
-- Kein Commit hier — wird von p_process_incoming_ba_data übernommen. -- Kein Commit hier — wird von p_process_incoming_ba_data übernommen.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_object_key Vollständiger OCI-Objektkey der zu verarbeitenden Datei -- Parameter: i_object_key Vollständiger OCI-Objektkey der zu verarbeitenden Datei
-- i_content Dateiinhalt als BLOB -- i_content Dateiinhalt als BLOB
-- i_target_folder Zielordner-Prefix für erfolgreich verarbeitete Dateien -- i_target_folder Zielordner-Prefix für erfolgreich verarbeitete Dateien
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Stub erstellt -- SCK 2026-04-08 Stub erstellt
-- SCK 2026-04-09 Implementierung: Aufruf f_import_ba_dokument, Wiedervorlage bei Fehler -- SCK 2026-04-09 Implementierung: Aufruf f_import_ba_dokument, Wiedervorlage bei Fehler
-- SCK 2026-04-09 Move nach erfolgreichem Import in p_import_ba_korrespondenz verschoben -- SCK 2026-04-09 Move nach erfolgreichem Import in p_import_ba_korrespondenz verschoben
@@ -110,31 +158,66 @@ create or replace package body pck_auto_import as
l_filename := substr(i_object_key, instr(i_object_key, '/', -1) + 1); l_filename := substr(i_object_key, instr(i_object_key, '/', -1) + 1);
l_file_size := dbms_lob.getlength(i_content); l_file_size := dbms_lob.getlength(i_content);
l_return := inkasso.pck_import.f_import_ba_dokument( pck_log.p_info(
i_datei => i_content i_module => c_log_module
,i_dateiname => l_filename ,i_action => l_log_action
,i_datei_groesse => l_file_size ,i_message => 'Import gestartet: "' || l_filename || '" (' || l_file_size || ' Bytes)'
,i_object_ref => i_object_key
); );
begin
l_return := inkasso.pck_import.f_import_ba_dokument(
i_datei => i_content
,i_dateiname => l_filename
,i_datei_groesse => l_file_size
);
exception when others
then
rollback;
pck_log.p_warn(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'Aufruf von pck_import.f_import_ba_dokument für Datei "' || l_filename || '" hat einen Fehler geworfen (' || SQLERRM || '). Erstelle wiedervorlage...'
,i_object_ref => i_object_key
);
-- Bei einem Import Fehler: Wiedervorlage für Sachbearbeiter erstellen & Fehlermeldung mit in die Wiedervorlage schreiben
p_create_wv_autonomous(i_swv_bemerkung => 'Bitte manuell Prüfen: Beim automatischen Import der BA-Datei "' || l_filename
|| '" ist folgender Fehler aufgetreten: "' || SQLERRM || '" (Siehe "' || i_object_key || '").'
);
raise;
end;
if l_return != 1 if l_return != 1
then then
pck_log.p_warn( pck_log.p_warn(
i_module => c_log_module i_module => c_log_module
,i_action => l_log_action ,i_action => l_log_action
,i_message => 'Import für Datei "' || l_filename || '" fehlgeschlagen (Rückgabe: ' || l_return || ') — Wiedervorlage erforderlich' ,i_message => 'Import für Datei "' || l_filename || '" fehlgeschlagen (Rückgabe: ' || l_return || ') — Wiedervorlage erforderlich'
,i_object_ref => i_object_key ,i_object_ref => i_object_key
); );
-- Wiedervorlage für Sachbearbeiter erstellen -- Wiedervorlage für Sachbearbeiter erstellen
pck_wiedervorlage.p_wiedervorlage_anlegen (i_swv_stp_id_art => pck_stammdaten.f_get_stp_id_by_programmid('WV_IMPORT_DATEV') -- TODO: neue WV Art? z.B. WV_IMPORT_BA_DATEN p_create_wv_autonomous(i_swv_bemerkung => 'Bitte manuell Prüfen: Die BA-Datei "' || l_filename || '" konnte nicht automatisch importiert werden (Siehe "' || i_object_key || '").'
,i_swv_wdatum => sysdate --TODO: welches Datum? sysdate?
,i_swv_bemerkung => 'Bitte manuell Prüfen: Die BA-Datei "' || l_filename || '" konnte nicht automatisch importiert werden (Siehe "' || i_object_key || '").'
,i_swv_mit_id_wsachbearbeiter => pck_system.f_get_par_wert_by_programmid('BA_IMPORT_SB_MIT_ID')
); );
raise_application_error(-20000, 'Import fehlgeschlagen: "' || l_filename || '" (Rückgabe: ' || l_return || ')'); pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'Wiedervorlage angelegt.'
,i_object_ref => i_object_key
);
raise_application_error(-20000, 'Import fehlgeschlagen: "' || l_filename || '" (Rückgabe: ' || l_return || ')');
end if; end if;
pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'Datei "' || l_filename || '" erfolgreich importiert.'
,i_object_ref => i_object_key
);
-- Datei in Verarbeitet-Ordner verschieben -- Datei in Verarbeitet-Ordner verschieben
pck_net_storage.p_move_object( pck_net_storage.p_move_object(
i_object_key => i_object_key i_object_key => i_object_key
@@ -153,13 +236,13 @@ create or replace package body pck_auto_import as
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Verarbeitet alle fertigen Eingangs-Batches aus dem OCI Eingangsordner (Netzlaufwerk). -- Beschreibung: Verarbeitet alle fertigen Eingangs-Batches aus dem OCI Eingangsordner (Netzlaufwerk).
-- Wird von ORDS-Endpunkt (von Quarkus Automaton) und aus Apex Automation aufgerufen. -- Wird von ORDS-Endpunkt (von Quarkus Automaton) und aus Apex Automation aufgerufen.
-- Pro Datei: Import + Move Commit; bei Exception: Rollback, Datei bleibt liegen, -- Pro Datei: Import + Move -> Commit; bei Exception: Rollback, Datei bleibt liegen,
-- nächste Datei wird trotzdem verarbeitet. -- nächste Datei wird trotzdem verarbeitet.
-- Nach dem Datei-Loop: DB-Marker immer löschen (verhindert erneuten Durchlauf). -- Nach dem Datei-Loop: DB-Marker immer löschen (verhindert erneuten Durchlauf).
-- Wenn danach noch Dateien im Ordner liegen: Sachbearbeiter(SB)-Marker anlegen damit Sachbearbeiter -- Wenn danach noch Dateien im Ordner liegen: Sachbearbeiter(SB)-Marker anlegen damit Sachbearbeiter
-- die übriggebliebenen Dateien manuell prüfen können. -- die übriggebliebenen Dateien manuell prüfen können.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Prozedur erstellt -- SCK 2026-04-08 Prozedur erstellt
-- SCK 2026-04-09 Fehlerbehandlung: Datei bleibt liegen, Fehler-Marker, kein erneuter Durchlauf -- SCK 2026-04-09 Fehlerbehandlung: Datei bleibt liegen, Fehler-Marker, kein erneuter Durchlauf
-- SCK 2026-04-09 SB-Marker statt l_had_errors-Flag; Move in p_import_ba_korrespondenz verschoben -- SCK 2026-04-09 SB-Marker statt l_had_errors-Flag; Move in p_import_ba_korrespondenz verschoben
@@ -178,11 +261,17 @@ create or replace package body pck_auto_import as
l_has_remaining_files boolean; l_has_remaining_files boolean;
l_log_action varchar2(512 char) := 'IMPORT_BA_DATA'; l_log_action varchar2(512 char) := 'IMPORT_BA_DATA';
begin begin
-- Zielordner Name zusammenstellen pck_log.p_info(
l_target_prefix := pck_system.f_get_par_wert_by_programmid('NETSTORE_BA_PREFIX') || 'Verarbeitet ' || to_char(sysdate, 'YYYY') || '/'; i_module => c_log_module
l_eingang_prefix := pck_system.f_get_par_wert_by_programmid('NETSTORE_BA_PREFIX') || pck_system.f_get_par_wert_by_programmid('NETSTORE_BA_IMPORT'); ,i_action => l_log_action
,i_message => 'BA-KORRESPONDENZEN IMPORT-START: Verarbeitung von eingehen BA Korrespondenzen gestartet'
);
-- Unterordner in eingangs-ordner auflisten (es gibt einen Ordner für jeden entpackte ZIP-Datei) -- Zielordner Name zusammenstellen
l_target_prefix := pck_system.f_get_par_wert_by_programmid('NETSTORE_TENANT_ID') || pck_system.f_get_par_wert_by_programmid('NETSTORE_BA_PREFIX') || pck_system.f_get_par_wert_by_programmid('NETSTORE_BA_KOR_ARC') || ' ' || to_char(sysdate, 'YYYY') || '/';
l_eingang_prefix := pck_system.f_get_par_wert_by_programmid('NETSTORE_TENANT_ID') || pck_system.f_get_par_wert_by_programmid('NETSTORE_BA_PREFIX') || pck_system.f_get_par_wert_by_programmid('NETSTORE_BA_KOR_IM');
-- Unterordner in eingangs-ordner auflisten (es gibt einen Ordner für jeden entpackte ZIP-Datei)
l_folders := pck_net_storage.f_list_objects( l_folders := pck_net_storage.f_list_objects(
i_parent_folder => l_eingang_prefix i_parent_folder => l_eingang_prefix
,i_include_subfolders => 'N' ,i_include_subfolders => 'N'
@@ -196,11 +285,25 @@ create or replace package body pck_auto_import as
continue; continue;
end if; end if;
pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'Datei-Ordner gefunden'
,i_object_ref => rec_folder.object_key
);
-- Der Marker ist eine Datei mit speziellem Namen, welche vom quarkus automaton in einen entpackten zip-ordner gelegt wird um zu signalisieren, dass alle Dateien des ZIPs erfolgreich in den ordner gelegt wurden. -- Der Marker ist eine Datei mit speziellem Namen, welche vom quarkus automaton in einen entpackten zip-ordner gelegt wird um zu signalisieren, dass alle Dateien des ZIPs erfolgreich in den ordner gelegt wurden.
-- Das verhindert die verarbeitung von unvollständig entpackten zips -- Das verhindert die verarbeitung von unvollständig entpackten zips
l_db_processing_marker_key := rec_folder.object_key || pck_system.f_get_par_wert_by_programmid('NETSTORE_MARKER_DB'); l_db_processing_marker_key := rec_folder.object_key || pck_system.f_get_par_wert_by_programmid('NETSTORE_MARKER_DB');
-- Marker prüfen: -20001 = nicht vorhanden → Upload noch nicht abgeschlossen pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'Marker für Ordner wird geprüft. Marker: ' || l_db_processing_marker_key || ' in Ordner: ' || rec_folder.object_key
,i_object_ref => rec_folder.object_key
);
-- Marker prüfen: -20001 = nicht vorhanden -> Upload noch nicht abgeschlossen
begin begin
l_meta := pck_net_storage.f_get_object_metadata(l_db_processing_marker_key); l_meta := pck_net_storage.f_get_object_metadata(l_db_processing_marker_key);
exception exception
@@ -208,12 +311,18 @@ create or replace package body pck_auto_import as
then then
if sqlcode = -20001 if sqlcode = -20001
then then
pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'Kein DB-Verarbeitungsmarker vorhanden — Entpackter ZIP-Ordner wird übersprungen (Upload noch nicht abgeschlossen)'
,i_object_ref => rec_folder.object_key
);
continue; continue;
end if; end if;
raise; raise;
end; end;
-- Zip-Namen aus Ordnerpfad ableiten: eingang/<zip-name>/ <zip-name> -- Zip-Namen aus Ordnerpfad ableiten: eingang/<zip-name>/ -> <zip-name>
l_zip_name := substr( l_zip_name := substr(
rec_folder.object_key rec_folder.object_key
,length(l_eingang_prefix) + 1 ,length(l_eingang_prefix) + 1
@@ -221,6 +330,13 @@ create or replace package body pck_auto_import as
); );
l_target_folder := l_target_prefix || l_zip_name || '/'; l_target_folder := l_target_prefix || l_zip_name || '/';
pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'ZIP-START: Verarbeitung von entpacktem ZIP-Ordner gestartet — Zielordner: "' || l_target_folder || '"'
,i_object_ref => rec_folder.object_key
);
-- Alle Dateien im Unterordner auflisten (inkl. Unterordner = alle Tiefen) -- Alle Dateien im Unterordner auflisten (inkl. Unterordner = alle Tiefen)
l_files := pck_net_storage.f_list_objects( l_files := pck_net_storage.f_list_objects(
i_parent_folder => rec_folder.object_key i_parent_folder => rec_folder.object_key
@@ -229,21 +345,28 @@ create or replace package body pck_auto_import as
for rec_file in (select object_key, is_folder from table(l_files)) for rec_file in (select object_key, is_folder from table(l_files))
loop loop
-- Marker und Pseudo-Ordner überspringen -- Marker und Pseudo-Ordner überspringen
if rec_file.object_key = l_db_processing_marker_key or rec_file.is_folder = 'Y' if rec_file.object_key = l_db_processing_marker_key or rec_file.is_folder = 'Y'
then then
continue; continue;
end if; end if;
begin begin
pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'BA-Datei wird verarbeitet'
,i_object_ref => rec_file.object_key
);
-- 1. Dateiinhalt laden -- 1. Dateiinhalt laden
l_file_content := pck_net_storage.f_download_object(rec_file.object_key); l_file_content := pck_net_storage.f_download_object(rec_file.object_key);
-- 2. Fachliche Verarbeitung + Move bei Erfolg (innerhalb p_import_ba_korrespondenz) -- 2. Fachliche Verarbeitung + Move bei Erfolg (innerhalb p_import_ba_korrespondenz)
p_import_ba_korrespondenz(rec_file.object_key, l_file_content, l_target_folder); p_import_ba_korrespondenz(rec_file.object_key, l_file_content, l_target_folder);
-- Commit pro Datei: OCI-Move ist nicht transaktional, daher DB-Änderungen sofort sichern -- Commit pro Datei: OCI-Move ist nicht transaktional, daher DB-Änderungen sofort sichern
-- sonst würde ein Fehler bei einer späteren Datei den DB-Import bereits verschobener Dateien zurückrollen -- sonst würde ein Fehler bei einer späteren Datei den DB-Import bereits verschobener Dateien zurückrollen
commit; commit;
exception exception
@@ -260,10 +383,17 @@ create or replace package body pck_auto_import as
end; end;
end loop; end loop;
-- DB-Marker immer entfernen — verhindert erneute Verarbeitung beim nächsten Lauf -- DB-Marker immer entfernen — verhindert erneute Verarbeitung beim nächsten Lauf
pck_net_storage.p_delete_object(l_db_processing_marker_key); pck_net_storage.p_delete_object(l_db_processing_marker_key);
-- Prüfen ob noch Dateien im Unterordner liegen (nicht erfolgreich importierte Dateien) pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'DB-Verarbeitungsmarker gelöscht'
,i_object_ref => l_db_processing_marker_key
);
-- Prüfen ob noch Dateien im Unterordner liegen (nicht erfolgreich importierte Dateien)
l_files := pck_net_storage.f_list_objects( l_files := pck_net_storage.f_list_objects(
i_parent_folder => rec_folder.object_key i_parent_folder => rec_folder.object_key
,i_include_subfolders => 'Y' ,i_include_subfolders => 'Y'
@@ -282,28 +412,38 @@ create or replace package body pck_auto_import as
if l_has_remaining_files if l_has_remaining_files
then then
-- Sachbearbeiter (SB)-Marker anlegen: signalisiert Sachbearbeitern, dass Dateien manuell geprüft werden müssen pck_log.p_warn(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'ZIP-ENDE: Entpackter ZIP-Ordner mit Fehlern abgeschlossen - mind. eine Datei konnte nicht importiert werden, SB-Marker wird hochgeladen...'
,i_object_ref => rec_folder.object_key
);
-- Sachbearbeiter (SB)-Marker anlegen: signalisiert Sachbearbeitern, dass Dateien manuell geprüft werden müssen
l_sb_marker_key := rec_folder.object_key || pck_system.f_get_par_wert_by_programmid('NETSTORE_MARKER_SB'); l_sb_marker_key := rec_folder.object_key || pck_system.f_get_par_wert_by_programmid('NETSTORE_MARKER_SB');
pck_net_storage.p_upload_object( pck_net_storage.p_upload_object(
i_object_key => l_sb_marker_key i_object_key => l_sb_marker_key
,i_content => empty_blob() ,i_content => empty_blob()
,i_content_type => 'application/octet-stream' ,i_content_type => 'application/octet-stream'
); );
pck_log.p_warn(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'Batch mit Fehlern abgeschlossen — mind. eine Datei konnte nicht importiert werden, SB-Marker gesetzt'
,i_object_ref => rec_folder.object_key
);
else else
pck_log.p_info( pck_log.p_info(
i_module => c_log_module i_module => c_log_module
,i_action => l_log_action ,i_action => l_log_action
,i_message => 'Batch abgeschlossen, alle Dateien erfolgreich importiert' ,i_message => 'ZIP-ENDE: Entpackter ZIP-Ordner abgeschlossen, alle Dateien erfolgreich importiert'
,i_object_ref => rec_folder.object_key ,i_object_ref => rec_folder.object_key
); );
end if; end if;
end loop; end loop;
pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'BA-KORRESPONDENZEN IMPORT-ENDE: Verarbeitung von eingehen BA Korrespondenzen abgeschlossen'
);
end p_process_incoming_ba_data; end p_process_incoming_ba_data;
end pck_auto_import; end pck_auto_import;

View File

@@ -1,6 +1,6 @@
create or replace package pck_auto_import as create or replace package pck_auto_import as
-- Von APEX Automation aufgerufen: verarbeitet offene OCI-Batches, stößt dann Quarkus an -- Von APEX Automation aufgerufen: verarbeitet offene OCI-Batches, stoert dann Quarkus an
procedure p_run_ba_korrespondenz_dateieingang_automation; procedure p_run_ba_korrespondenz_dateieingang_automation;
-- Von ORDS-Endpunkt aufgerufen (net_storage/process_incoming_ba_data): importiert Dateien aus OCI -- Von ORDS-Endpunkt aufgerufen (net_storage/process_incoming_ba_data): importiert Dateien aus OCI

View File

@@ -9,18 +9,18 @@ create or replace package body pck_log as
,i_object_ref in varchar2 default null ,i_object_ref in varchar2 default null
) )
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Interne Hilfsprozedur — schreibt einen Log-Eintrag in lg_app_log. -- Beschreibung: Interne Hilfsprozedur — schreibt einen Log-Eintrag in lg_app_log.
-- Verwendet autonomous_transaction, damit der Commit unabhängig vom Aufrufer erfolgt. -- Verwendet autonomous_transaction, damit der Commit unabhängig vom Aufrufer erfolgt.
-- Wird ausschließlich von p_info, p_warn und p_error aufgerufen. -- Wird ausschließlich von p_info, p_warn und p_error aufgerufen.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_level Log-Level (INFO, WARN, ERROR) -- Parameter: i_level Log-Level (INFO, WARN, ERROR)
-- i_module Aufgerufenes Modul / Package -- i_module Aufgerufenes Modul / Package
-- i_action Aktion innerhalb des Moduls -- i_action Aktion innerhalb des Moduls
-- i_message Kurze Meldung -- i_message Kurze Meldung
-- i_detail Optionaler Langtext (Stack Trace, JSON, etc.) -- i_detail Optionaler Langtext (Stack Trace, JSON, etc.)
-- i_object_ref Optionaler Objektbezug (z.B. Dateiname, Primärschlüssel) -- i_object_ref Optionaler Objektbezug (z.B. Dateiname, Primärschlüssel)
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Prozedur erstellt -- SCK 2026-04-08 Prozedur erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -73,9 +73,9 @@ create or replace package body pck_log as
-- Parameter: i_module Aufgerufenes Modul / Package -- Parameter: i_module Aufgerufenes Modul / Package
-- i_action Aktion innerhalb des Moduls -- i_action Aktion innerhalb des Moduls
-- i_message Kurze Meldung -- i_message Kurze Meldung
-- i_object_ref Optionaler Objektbezug (z.B. Dateiname, Primärschlüssel) -- i_object_ref Optionaler Objektbezug (z.B. Dateiname, Primärschlüssel)
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Prozedur erstellt -- SCK 2026-04-08 Prozedur erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -101,9 +101,9 @@ create or replace package body pck_log as
-- Parameter: i_module Aufgerufenes Modul / Package -- Parameter: i_module Aufgerufenes Modul / Package
-- i_action Aktion innerhalb des Moduls -- i_action Aktion innerhalb des Moduls
-- i_message Kurze Meldung -- i_message Kurze Meldung
-- i_object_ref Optionaler Objektbezug (z.B. Dateiname, Primärschlüssel) -- i_object_ref Optionaler Objektbezug (z.B. Dateiname, Primärschlüssel)
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Prozedur erstellt -- SCK 2026-04-08 Prozedur erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -131,9 +131,9 @@ create or replace package body pck_log as
-- i_action Aktion innerhalb des Moduls -- i_action Aktion innerhalb des Moduls
-- i_message Kurze Fehlerbeschreibung -- i_message Kurze Fehlerbeschreibung
-- i_detail Optionaler Langtext (Stack Trace, JSON, etc.) -- i_detail Optionaler Langtext (Stack Trace, JSON, etc.)
-- i_object_ref Optionaler Objektbezug (z.B. Dateiname, Primärschlüssel) -- i_object_ref Optionaler Objektbezug (z.B. Dateiname, Primärschlüssel)
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Prozedur erstellt -- SCK 2026-04-08 Prozedur erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is

View File

@@ -9,15 +9,15 @@ create or replace package body pck_net_storage as
,i_action in varchar2 default null ,i_action in varchar2 default null
) return varchar2 ) return varchar2
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Baut die vollständige OCI Object Storage URL aus den Konfigurationsparametern. -- Beschreibung: Baut die vollständige OCI Object Storage URL aus den Konfigurationsparametern.
-- Entweder für eine Bucket-Action, ein einzelnes Objekt oder den Bucket-Root. -- Entweder für eine Bucket-Action, ein einzelnes Objekt oder den Bucket-Root.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_object_key Objektschlüssel (Pfad im Bucket); null für Bucket-Root oder Action-URL -- Parameter: i_object_key Objektschlüssel (Pfad im Bucket); null für Bucket-Root oder Action-URL
-- i_action OCI Bucket-Action (z.B. renameObject); null für Objekt-URL -- i_action OCI Bucket-Action (z.B. renameObject); null für Objekt-URL
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Rückgabe: Vollständige URL als VARCHAR2 -- Rückgabe: Vollständige URL als VARCHAR2
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Funktion erstellt -- SCK 2026-04-08 Funktion erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -35,7 +35,7 @@ create or replace package body pck_net_storage as
return l_base || '/actions/' || i_action; return l_base || '/actions/' || i_action;
elsif i_object_key is not null elsif i_object_key is not null
then then
-- Sonderzeichen kodieren, Schrägstriche im Key unverändert lassen -- Sonderzeichen kodieren, Schrägstriche im Key unverändert lassen
return l_base || '/o/' || utl_url.escape(i_object_key, false); return l_base || '/o/' || utl_url.escape(i_object_key, false);
else else
return l_base || '/o'; return l_base || '/o';
@@ -57,18 +57,19 @@ create or replace package body pck_net_storage as
procedure p_assert_allowed (i_object_key in varchar2) procedure p_assert_allowed (i_object_key in varchar2)
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Prüft den Objektschlüssel auf Gültigkeit, Path-Traversal-Angriffe und Tenant-Scope. -- Beschreibung: Prüft den Objektschlüssel auf Gültigkeit, Path-Traversal-Angriffe und Tenant-Scope.
-- Wirft Application Error -20008 bei null-Key, -20004 bei Path Traversal, -- Wirft Application Error -20008 bei null-Key, -20004 bei Path Traversal,
-- -20005 bei Scope-Verletzung. -- -20005 bei Scope-Verletzung.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_object_key Zu prüfender Objektschlüssel -- Parameter: i_object_key Zu prüfender Objektschlüssel
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Prozedur erstellt -- SCK 2026-04-08 Prozedur erstellt
-- SCK 2026-04-10 Null-Prüfung und führender-Slash-Check ergänzt -- SCK 2026-04-10 Null-Prüfung und führender-Slash-Check ergänzt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
l_tenant_prefix varchar2(256); l_tenant_prefix varchar2(256);
l_log_action varchar2(256) := 'ASSERT_ALLOWED';
begin begin
if i_object_key is null if i_object_key is null
then then
@@ -82,6 +83,15 @@ create or replace package body pck_net_storage as
l_tenant_prefix := pck_system.f_get_par_wert_by_programmid('NETSTORE_TENANT_ID'); l_tenant_prefix := pck_system.f_get_par_wert_by_programmid('NETSTORE_TENANT_ID');
/*
-- Log for debugging Prefix check
pck_log.p_info(
i_module => c_log_module
,i_action => l_log_action
,i_message => 'Checking Prefix: Netstore tenant-Prefix: ' || l_tenant_prefix || '; Accessed Prefix: ' || substr(i_object_key, 1, length(l_tenant_prefix)) || '; Accessed Object: ' || i_object_key
);
*/
if l_tenant_prefix is not null and length(l_tenant_prefix) > 0 if l_tenant_prefix is not null and length(l_tenant_prefix) > 0
then then
if substr(i_object_key, 1, length(l_tenant_prefix)) != l_tenant_prefix if substr(i_object_key, 1, length(l_tenant_prefix)) != l_tenant_prefix
@@ -99,21 +109,21 @@ create or replace package body pck_net_storage as
,i_content_type in varchar2 default null ,i_content_type in varchar2 default null
) return clob ) return clob
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Führt einen HTTP-Request gegen die OCI Object Storage API aus. -- Beschreibung: Führt einen HTTP-Request gegen die OCI Object Storage API aus.
-- Wertet den HTTP-Statuscode aus und löst bei Fehler einen Application Error aus. -- Wertet den HTTP-Statuscode aus und löst bei Fehler einen Application Error aus.
-- Authentifizierung erfolgt über APEX Web Credential (NETSTORE_CRED_ID). -- Authentifizierung erfolgt über APEX Web Credential (NETSTORE_CRED_ID).
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_method HTTP-Methode (GET, PUT, DELETE, POST, HEAD) -- Parameter: i_method HTTP-Methode (GET, PUT, DELETE, POST, HEAD)
-- i_url Vollständige Ziel-URL -- i_url Vollständige Ziel-URL
-- i_body_clob Optionaler Request-Body als CLOB (z.B. JSON) -- i_body_clob Optionaler Request-Body als CLOB (z.B. JSON)
-- i_body_blob Optionaler Request-Body als BLOB (Binärinhalt) -- i_body_blob Optionaler Request-Body als BLOB (Binärinhalt)
-- i_content_type Optionaler Content-Type Header -- i_content_type Optionaler Content-Type Header
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Rückgabe: Response-Body als CLOB (bei HEAD-Requests leer) -- Rückgabe: Response-Body als CLOB (bei HEAD-Requests leer)
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Funktion erstellt -- SCK 2026-04-08 Funktion erstellt
-- SCK 2026-04-16 empty_blob()/empty_clob() als Default entfernt — APEX OCI-Signing braucht null für nicht genutzte Body-Parameter -- SCK 2026-04-16 empty_blob()/empty_clob() als Default entfernt — APEX OCI-Signing braucht null für nicht genutzte Body-Parameter
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
l_response clob; l_response clob;
@@ -121,6 +131,8 @@ create or replace package body pck_net_storage as
l_header_index number := 1; l_header_index number := 1;
l_content_length number; l_content_length number;
begin begin
-- headers zurücksetzen - nur zur Sicherheit, damit keine alten Header übertragen werden.
apex_web_service.g_request_headers.delete;
if i_content_type is not null if i_content_type is not null
then then
@@ -149,10 +161,13 @@ create or replace package body pck_net_storage as
l_header_index := l_header_index + 1; l_header_index := l_header_index + 1;
*/ */
-- nur für BLOB (z.B. leerer Ordner) Content-Length setzen -- nur für leere BLOBs (z.B. leerer Ordner) Content-Length setzen
if i_body_blob is not null -- bei nicht leeren blob setzt apex_web_service.make_rest_request den content-length header automatisch, doppeltes setzen führt aber zu einem HTTP-400 API Fehler
-- bei leeren blobs (empty_blob()) wird er aber nicht automatisch gesetzt, daher müssen wir ihn manuell setzen
if i_body_blob is not null
and dbms_lob.getlength(i_body_blob) = 0
then then
l_content_length := coalesce(dbms_lob.getlength(i_body_blob), 0); l_content_length := 0; -- coalesce(dbms_lob.getlength(i_body_blob), 0);
apex_web_service.g_request_headers(l_header_index).name := 'Content-Length'; apex_web_service.g_request_headers(l_header_index).name := 'Content-Length';
apex_web_service.g_request_headers(l_header_index).value := l_content_length; apex_web_service.g_request_headers(l_header_index).value := l_content_length;
l_header_index := l_header_index + 1; l_header_index := l_header_index + 1;
@@ -162,8 +177,8 @@ create or replace package body pck_net_storage as
if i_body_clob is not null if i_body_clob is not null
then then
apex_debug.info('Clob Request Body used:'); --apex_debug.info('Clob Request Body used:');
apex_debug.info(i_body_clob); --apex_debug.info(i_body_clob);
l_response := apex_web_service.make_rest_request( l_response := apex_web_service.make_rest_request(
p_url => i_url p_url => i_url
,p_http_method => i_method ,p_http_method => i_method
@@ -172,7 +187,7 @@ create or replace package body pck_net_storage as
,p_wallet_path => pck_system.f_get_par_wert_by_programmid('NETSTORE_WALLET_PATH') ,p_wallet_path => pck_system.f_get_par_wert_by_programmid('NETSTORE_WALLET_PATH')
); );
else else
apex_debug.info('BLOB Request Body used! Length: ' || dbms_lob.getlength(i_body_blob)); --apex_debug.info('BLOB Request Body used! Length: ' || dbms_lob.getlength(i_body_blob));
l_response := apex_web_service.make_rest_request( l_response := apex_web_service.make_rest_request(
p_url => i_url p_url => i_url
,p_http_method => i_method ,p_http_method => i_method
@@ -202,7 +217,7 @@ create or replace package body pck_net_storage as
return l_response; return l_response;
end f_make_request; end f_make_request;
-- Interne Implementierung ohne Rechteprüfung — wird von f_list_objects und p_delete_folder (Leerprüfung) genutzt -- Interne Implementierung ohne Rechteprüfung — wird von f_list_objects und p_delete_folder (Leerprüfung) genutzt
function f_list_objects_internal ( function f_list_objects_internal (
i_parent_folder in varchar2 i_parent_folder in varchar2
,i_include_subfolders in varchar2 ,i_include_subfolders in varchar2
@@ -210,18 +225,18 @@ create or replace package body pck_net_storage as
,i_limit in number ,i_limit in number
) return t_net_storage_tab ) return t_net_storage_tab
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Listet Objekte und Unterordner im Bucket ohne Rechte- oder Scope-Prüfung. -- Beschreibung: Listet Objekte und Unterordner im Bucket ohne Rechte- oder Scope-Prüfung.
-- Paginiert automatisch über nextStartWith bis alle Ergebnisse geladen sind. -- Paginiert automatisch über nextStartWith bis alle Ergebnisse geladen sind.
-- Wird von f_list_objects (öffentlich) und p_delete_folder (Leerprüfung) intern genutzt. -- Wird von f_list_objects (öffentlich) und p_delete_folder (Leerprüfung) intern genutzt.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_parent_folder Ordnerpfad im Bucket (z.B. eingang/) -- Parameter: i_parent_folder Ordnerpfad im Bucket (z.B. eingang/)
-- i_include_subfolders 'Y' = alle Dateien rekursiv, 'N' = nur direkte Kinder des Ordners -- i_include_subfolders 'Y' = alle Dateien rekursiv, 'N' = nur direkte Kinder des Ordners
-- i_start_with Optionaler Startpunkt für Paginierung -- i_start_with Optionaler Startpunkt für Paginierung
-- i_limit Maximale Anzahl Ergebnisse (0 = unbegrenzt) -- i_limit Maximale Anzahl Ergebnisse (0 = unbegrenzt)
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Rückgabe: Collection t_net_storage_tab mit allen gefundenen Objekten -- Rückgabe: Collection t_net_storage_tab mit allen gefundenen Objekten
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Funktion erstellt -- SCK 2026-04-08 Funktion erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -273,7 +288,7 @@ create or replace package body pck_net_storage as
,l_obj_path.path ,l_obj_path.path
,l_obj_path.filename ,l_obj_path.filename
-- Explizit angelegte Ordner sind Zero-Byte-Objekte mit trailing /; -- Explizit angelegte Ordner sind Zero-Byte-Objekte mit trailing /;
-- size, last_modified und etag sind für Ordner nicht relevant -- size, last_modified und etag sind für Ordner nicht relevant
,(case when rec.object_name like '%/' then null else rec.object_size end) ,(case when rec.object_name like '%/' then null else rec.object_size end)
,(case when rec.object_name like '%/' then null else to_date(substr(rec.last_modified, 1, 19), 'YYYY-MM-DD"T"HH24:MI:SS') end) ,(case when rec.object_name like '%/' then null else to_date(substr(rec.last_modified, 1, 19), 'YYYY-MM-DD"T"HH24:MI:SS') end)
,(case when rec.object_name like '%/' then 'Y' else 'N' end) ,(case when rec.object_name like '%/' then 'Y' else 'N' end)
@@ -320,7 +335,7 @@ create or replace package body pck_net_storage as
end loop; end loop;
end if; end if;
-- Nächste Seite prüfen -- Nächste Seite prüfen
if not l_done if not l_done
then then
l_next_start := json_value(l_response, '$.nextStartWith'); l_next_start := json_value(l_response, '$.nextStartWith');
@@ -336,9 +351,9 @@ create or replace package body pck_net_storage as
-- Implizite Ordner aus Object-Keys ableiten. -- Implizite Ordner aus Object-Keys ableiten.
-- Die OCI-API liefert virtuelle Ordner (nie als Zero-Byte-Objekt angelegt) nur -- Die OCI-API liefert virtuelle Ordner (nie als Zero-Byte-Objekt angelegt) nur
-- über $.prefixes, und auch nur wenn delimiter gesetzt ist. Bei rekursivem Abruf -- über $.prefixes, und auch nur wenn delimiter gesetzt ist. Bei rekursivem Abruf
-- fehlen sie daher komplett. Wir leiten alle Zwischenpfade aus den Object-Keys ab -- fehlen sie daher komplett. Wir leiten alle Zwischenpfade aus den Object-Keys ab
-- und ergänzen fehlende Ordner-Einträge. -- und ergänzen fehlende Ordner-Einträge.
declare declare
l_new_folders apex_t_varchar2; l_new_folders apex_t_varchar2;
begin begin
@@ -349,16 +364,16 @@ create or replace package body pck_net_storage as
-- --
-- connect by level iteriert von 1 bis zur Anzahl der Slashes im Key. -- connect by level iteriert von 1 bis zur Anzahl der Slashes im Key.
-- instr(..., '/', 1, level) liefert die Position des n-ten Slashes. -- instr(..., '/', 1, level) liefert die Position des n-ten Slashes.
-- substr(..., 1, <position>) schneidet den Key bis einschließlich -- substr(..., 1, <position>) schneidet den Key bis einschließlich
-- dieses Slashes ab — das Ergebnis ist der Ordnerpfad auf Ebene n. -- dieses Slashes ab — das Ergebnis ist der Ordnerpfad auf Ebene n.
-- --
-- Beispiel für 'mandant/Eingang/batch-001/datei.pdf' (3 Slashes): -- Beispiel für 'mandant/Eingang/batch-001/datei.pdf' (3 Slashes):
-- level 1 'mandant/' -- level 1 -> 'mandant/'
-- level 2 'mandant/Eingang/' -- level 2 -> 'mandant/Eingang/'
-- level 3 'mandant/Eingang/batch-001/' -- level 3 -> 'mandant/Eingang/batch-001/'
-- --
-- prior object_key = object_key : bindet jede Zeile an sich selbst, -- prior object_key = object_key : bindet jede Zeile an sich selbst,
-- damit connect by die Levels pro Zeile unabhängig hochzählt. -- damit connect by die Levels pro Zeile unabhängig hochzählt.
-- prior sys_guid() is not null : verhindert Cycle-Detection-Fehler, -- prior sys_guid() is not null : verhindert Cycle-Detection-Fehler,
-- da keine echte Eltern-Kind-Beziehung vorliegt. -- da keine echte Eltern-Kind-Beziehung vorliegt.
select substr(r.object_key, 1, instr(r.object_key, '/', 1, level)) as folder_path select substr(r.object_key, 1, instr(r.object_key, '/', 1, level)) as folder_path
@@ -369,14 +384,14 @@ create or replace package body pck_net_storage as
and prior sys_guid() is not null and prior sys_guid() is not null
) )
-- Nur Pfade unterhalb des Parent-Folders behalten: -- Nur Pfade unterhalb des Parent-Folders behalten:
-- like-Bedingung schließt Vorfahren-Pfade aus (z.B. 'mandant/', 'mandant/Eingang/' -- like-Bedingung schließt Vorfahren-Pfade aus (z.B. 'mandant/', 'mandant/Eingang/'
-- wenn der Parent-Folder 'mandant/Eingang/batch/' ist). -- wenn der Parent-Folder 'mandant/Eingang/batch/' ist).
-- != schließt den Parent-Folder selbst aus. -- != schließt den Parent-Folder selbst aus.
-- Bei null-Parent-Folder (Bucket-Root): like '%' = immer wahr, chr(0) passt -- Bei null-Parent-Folder (Bucket-Root): like '%' = immer wahr, chr(0) passt
-- auf keinen gültigen Key beide Bedingungen greifen nicht. -- auf keinen gültigen Key -> beide Bedingungen greifen nicht.
where folder_path like nvl(l_parent_folder, '') || '%' where folder_path like nvl(l_parent_folder, '') || '%'
and folder_path != nvl(l_parent_folder, chr(0)) and folder_path != nvl(l_parent_folder, chr(0))
-- Bereits vorhandene Ordner-Einträge ausschließen (explizit angelegte -- Bereits vorhandene Ordner-Einträge ausschließen (explizit angelegte
-- Zero-Byte-Objekte oder via $.prefixes gelieferte virtuelle Ordner). -- Zero-Byte-Objekte oder via $.prefixes gelieferte virtuelle Ordner).
minus minus
select object_key select object_key
@@ -402,18 +417,18 @@ create or replace package body pck_net_storage as
return l_result; return l_result;
end f_list_objects_internal; end f_list_objects_internal;
-- ==================== Öffentliche Funktionen ==================== -- ==================== Öffentliche Funktionen ====================
function f_split_object_key (i_object_key in varchar2) return t_object_path function f_split_object_key (i_object_key in varchar2) return t_object_path
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Extrahiert Pfad und Dateiname aus einem OCI-Objektschlüssel. -- Beschreibung: Extrahiert Pfad und Dateiname aus einem OCI-Objektschlüssel.
-- Bei Ordner-Keys (trailing Slash) wird der Ordnername als Dateiname zurückgegeben. -- Bei Ordner-Keys (trailing Slash) wird der Ordnername als Dateiname zurückgegeben.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_object_key Vollständiger Objektschlüssel (z.B. mandant/Eingang/Import/datei.pdf) -- Parameter: i_object_key Vollständiger Objektschlüssel (z.B. mandant/Eingang/Import/datei.pdf)
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Rückgabe: t_object_path Record mit path (inkl. trailing Slash) und filename -- Rückgabe: t_object_path Record mit path (inkl. trailing Slash) und filename
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-09 Funktion erstellt -- SCK 2026-04-09 Funktion erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -445,16 +460,16 @@ create or replace package body pck_net_storage as
,i_limit in number default 0 ,i_limit in number default 0
) return t_net_storage_tab ) return t_net_storage_tab
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Listet Objekte und Unterordner im Bucket mit Rechteprüfung und Scope-Validierung. -- Beschreibung: Listet Objekte und Unterordner im Bucket mit Rechteprüfung und Scope-Validierung.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_parent_folder Ordnerpfad im Bucket (z.B. eingang/) -- Parameter: i_parent_folder Ordnerpfad im Bucket (z.B. eingang/)
-- i_include_subfolders 'Y' = alle Dateien rekursiv inkl. Unterordner, 'N' = nur direkte Dateien im Ordner (Standard) -- i_include_subfolders 'Y' = alle Dateien rekursiv inkl. Unterordner, 'N' = nur direkte Dateien im Ordner (Standard)
-- i_start_with Optionaler Startpunkt für Paginierung -- i_start_with Optionaler Startpunkt für Paginierung
-- i_limit Maximale Anzahl Ergebnisse (0 = unbegrenzt) -- i_limit Maximale Anzahl Ergebnisse (0 = unbegrenzt)
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Rückgabe: Collection t_net_storage_tab mit allen gefundenen Objekten -- Rückgabe: Collection t_net_storage_tab mit allen gefundenen Objekten
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Funktion erstellt -- SCK 2026-04-08 Funktion erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -470,13 +485,13 @@ create or replace package body pck_net_storage as
function f_download_object (i_object_key in varchar2) return blob function f_download_object (i_object_key in varchar2) return blob
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Lädt ein einzelnes Objekt aus dem OCI Bucket als BLOB herunter. -- Beschreibung: Lädt ein einzelnes Objekt aus dem OCI Bucket als BLOB herunter.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_object_key Vollständiger Objektschlüssel im Bucket -- Parameter: i_object_key Vollständiger Objektschlüssel im Bucket
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Rückgabe: Dateiinhalt als BLOB -- Rückgabe: Dateiinhalt als BLOB
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Funktion erstellt -- SCK 2026-04-08 Funktion erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -486,12 +501,15 @@ create or replace package body pck_net_storage as
pck_mitarbeiterrecht.p_hat_recht('LESEN_ALLES'); pck_mitarbeiterrecht.p_hat_recht('LESEN_ALLES');
p_assert_allowed(i_object_key); p_assert_allowed(i_object_key);
-- Wir nutzen hier direkt apex_web_service.make_rest_request_b, statt der internen f_make_request funktion, da wir nur hier einen blob statt clob return wert brauchen und eine extra
l_response := apex_web_service.make_rest_request_b( l_response := apex_web_service.make_rest_request_b(
p_url => f_build_url(i_object_key) p_url => f_build_url(i_object_key)
,p_http_method => 'GET' ,p_http_method => 'GET'
,p_credential_static_id => pck_system.f_get_par_wert_by_programmid('NETSTORE_CRED_ID') ,p_credential_static_id => pck_system.f_get_par_wert_by_programmid('NETSTORE_CRED_ID')
,p_wallet_path => pck_system.f_get_par_wert_by_programmid('NETSTORE_WALLET_PATH')
); );
l_status := apex_web_service.g_status_code; l_status := apex_web_service.g_status_code;
if l_status = 404 if l_status = 404
@@ -514,13 +532,13 @@ create or replace package body pck_net_storage as
,i_content_type in varchar2 ,i_content_type in varchar2
) )
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Lädt ein Objekt in den OCI Bucket hoch (PUT). Überschreibt vorhandene Objekte. -- Beschreibung: Lädt ein Objekt in den OCI Bucket hoch (PUT). Überschreibt vorhandene Objekte.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_object_key Zielpfad im Bucket -- Parameter: i_object_key Zielpfad im Bucket
-- i_content Dateiinhalt als BLOB -- i_content Dateiinhalt als BLOB
-- i_content_type MIME-Type des Inhalts (z.B. application/octet-stream) -- i_content_type MIME-Type des Inhalts (z.B. application/octet-stream)
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Prozedur erstellt -- SCK 2026-04-08 Prozedur erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -532,16 +550,15 @@ create or replace package body pck_net_storage as
if substr(i_object_key, -1) = '/' if substr(i_object_key, -1) = '/'
then then
raise_application_error(-20012, 'Object Key darf nicht mit / enden — zum Anlegen von Ordnern p_create_folder verwenden'); raise_application_error(-20012, 'Object Key darf nicht mit / enden — zum Anlegen von Ordnern p_create_folder verwenden');
end if; end if;
-- TEST l_response := f_make_request(
--l_response := f_make_request( i_method => 'PUT'
-- i_method => 'PUT' ,i_url => f_build_url(i_object_key)
-- ,i_url => f_build_url(i_object_key) ,i_body_blob => i_content
-- ,i_body_blob => i_content ,i_content_type => i_content_type
-- ,i_content_type => i_content_type );
--);
l_obj_path := f_split_object_key(i_object_key); l_obj_path := f_split_object_key(i_object_key);
pck_log.p_info( pck_log.p_info(
@@ -554,11 +571,11 @@ create or replace package body pck_net_storage as
procedure p_delete_object (i_object_key in varchar2) procedure p_delete_object (i_object_key in varchar2)
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Löscht ein einzelnes Objekt aus dem OCI Bucket (DELETE). -- Beschreibung: Löscht ein einzelnes Objekt aus dem OCI Bucket (DELETE).
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_object_key Vollständiger Objektschlüssel im Bucket -- Parameter: i_object_key Vollständiger Objektschlüssel im Bucket
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Prozedur erstellt -- SCK 2026-04-08 Prozedur erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -577,21 +594,21 @@ create or replace package body pck_net_storage as
pck_log.p_info( pck_log.p_info(
i_module => c_log_module i_module => c_log_module
,i_action => 'DELETE' ,i_action => 'DELETE'
,i_message => 'Datei "' || l_obj_path.filename || '" gelöscht | Ordner: ' || l_obj_path.path ,i_message => 'Datei "' || l_obj_path.filename || '" gelöscht | Ordner: ' || l_obj_path.path
,i_object_ref => i_object_key ,i_object_ref => i_object_key
); );
end p_delete_object; end p_delete_object;
procedure p_delete_folder (i_folder_key in varchar2) procedure p_delete_folder (i_folder_key in varchar2)
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Löscht einen leeren Ordner im OCI Bucket. -- Beschreibung: Löscht einen leeren Ordner im OCI Bucket.
-- Schlägt fehl, wenn noch Objekte oder Unterordner vorhanden sind. -- Schlägt fehl, wenn noch Objekte oder Unterordner vorhanden sind.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_folder_key Kompletter Ordner name inkl. Pfad (z.B. eingang/batch-001/) -- Parameter: i_folder_key Kompletter Ordner name inkl. Pfad (z.B. eingang/batch-001/)
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Prozedur erstellt -- SCK 2026-04-08 Prozedur erstellt
-- SCK 2026-04-10 Rekursives Löschen entfernt — Ordner muss leer sein -- SCK 2026-04-10 Rekursives Löschen entfernt — Ordner muss leer sein
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
l_objects t_net_storage_tab; l_objects t_net_storage_tab;
@@ -603,7 +620,7 @@ create or replace package body pck_net_storage as
pck_mitarbeiterrecht.p_hat_recht('ADMIN'); pck_mitarbeiterrecht.p_hat_recht('ADMIN');
p_assert_allowed(l_prefix); p_assert_allowed(l_prefix);
-- Direkte Kinder prüfen (Dateien und Unterordner) -- Direkte Kinder prüfen (Dateien und Unterordner)
l_objects := f_list_objects_internal( l_objects := f_list_objects_internal(
i_parent_folder => l_prefix i_parent_folder => l_prefix
,i_include_subfolders => 'N' ,i_include_subfolders => 'N'
@@ -612,14 +629,14 @@ create or replace package body pck_net_storage as
); );
/* /*
apex_debug.info('p_delete_folder: prefix=%s, Anzahl gefundene Einträge=%s', l_prefix, l_objects.count); apex_debug.info('p_delete_folder: prefix=%s, Anzahl gefundene Einträge=%s', l_prefix, l_objects.count);
for i in 1 .. l_objects.count for i in 1 .. l_objects.count
loop loop
apex_debug.info(' [%s] key=%s | is_folder=%s', i, l_objects(i).object_key, l_objects(i).is_folder); apex_debug.info(' [%s] key=%s | is_folder=%s', i, l_objects(i).object_key, l_objects(i).is_folder);
end loop; end loop;
*/ */
-- Den Ordner selbst (object_key = l_prefix) aus der Zählung ausschließen -- Den Ordner selbst (object_key = l_prefix) aus der Zählung ausschließen
select count(*) select count(*)
into l_count into l_count
from table(l_objects) from table(l_objects)
@@ -627,10 +644,10 @@ create or replace package body pck_net_storage as
if l_count > 0 if l_count > 0
then then
raise_application_error(-20017, 'Ordner ist nicht leer und kann nicht gelöscht werden'); raise_application_error(-20017, 'Ordner ist nicht leer und kann nicht gelöscht werden');
end if; end if;
-- Ordner-Objekt selbst löschen -- Ordner-Objekt selbst löschen
l_response := f_make_request( l_response := f_make_request(
i_method => 'DELETE' i_method => 'DELETE'
,i_url => f_build_url(l_prefix) ,i_url => f_build_url(l_prefix)
@@ -640,7 +657,7 @@ create or replace package body pck_net_storage as
pck_log.p_info( pck_log.p_info(
i_module => c_log_module i_module => c_log_module
,i_action => 'DELETE_FOLDER' ,i_action => 'DELETE_FOLDER'
,i_message => 'Ordner "' || l_obj_path.filename || '" gelöscht | Pfad: ' || l_obj_path.path ,i_message => 'Ordner "' || l_obj_path.filename || '" gelöscht | Pfad: ' || l_obj_path.path
,i_object_ref => l_prefix ,i_object_ref => l_prefix
); );
end p_delete_folder; end p_delete_folder;
@@ -653,10 +670,10 @@ create or replace package body pck_net_storage as
-- Beschreibung: Benennt ein Objekt innerhalb desselben Verzeichnisses um. -- Beschreibung: Benennt ein Objekt innerhalb desselben Verzeichnisses um.
-- Verwendet die OCI renameObject-Action (kein physisches Kopieren). -- Verwendet die OCI renameObject-Action (kein physisches Kopieren).
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_object_key Vollständiger Objektschlüssel des Quelldatei -- Parameter: i_object_key Vollständiger Objektschlüssel des Quelldatei
-- i_new_name Neuer Dateiname (ohne Pfad) -- i_new_name Neuer Dateiname (ohne Pfad)
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Prozedur erstellt -- SCK 2026-04-08 Prozedur erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -675,7 +692,7 @@ create or replace package body pck_net_storage as
if instr(i_new_name, '/') > 0 if instr(i_new_name, '/') > 0
then then
raise_application_error(-20014, 'Dateiname darf keinen Schrägstrich enthalten — zum Verschieben explizite Verschieben-Funktion verwenden'); raise_application_error(-20014, 'Dateiname darf keinen Schrägstrich enthalten — zum Verschieben explizite Verschieben-Funktion verwenden');
end if; end if;
l_obj_path := f_split_object_key(i_object_key); l_obj_path := f_split_object_key(i_object_key);
@@ -684,7 +701,7 @@ create or replace package body pck_net_storage as
if l_new_key = i_object_key if l_new_key = i_object_key
then then
raise_application_error(-20016, 'Der Dateiname darf beim Umbenennen nicht unverändert bleiben.'); raise_application_error(-20016, 'Der Dateiname darf beim Umbenennen nicht unverändert bleiben.');
end if; end if;
select json_object( select json_object(
@@ -716,12 +733,12 @@ create or replace package body pck_net_storage as
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Verschiebt ein Objekt in einen anderen Ordner im selben Bucket. -- Beschreibung: Verschiebt ein Objekt in einen anderen Ordner im selben Bucket.
-- Verwendet die OCI renameObject-Action (kein physisches Kopieren). -- Verwendet die OCI renameObject-Action (kein physisches Kopieren).
-- Der Dateiname bleibt erhalten; nur der Pfad ändert sich. -- Der Dateiname bleibt erhalten; nur der Pfad ändert sich.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_object_key Vollständiger Objektschlüssel der Quelldatei -- Parameter: i_object_key Vollständiger Objektschlüssel der Quelldatei
-- i_target_prefix Zielpräfix inkl. trailing Slash (z.B. verarbeitet/batch-001/) -- i_target_prefix Zielpräfix inkl. trailing Slash (z.B. verarbeitet/batch-001/)
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Prozedur erstellt -- SCK 2026-04-08 Prozedur erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -737,7 +754,7 @@ create or replace package body pck_net_storage as
if i_target_prefix is null if i_target_prefix is null
then then
raise_application_error(-20015, 'Zielpräfix darf nicht null sein'); raise_application_error(-20015, 'Zielpräfix darf nicht null sein');
end if; end if;
l_target_prefix := f_normalize_prefix(i_target_prefix); l_target_prefix := f_normalize_prefix(i_target_prefix);
@@ -788,10 +805,10 @@ create or replace package body pck_net_storage as
-- Beschreibung: Legt einen neuen Ordner im OCI Bucket an. -- Beschreibung: Legt einen neuen Ordner im OCI Bucket an.
-- Ordner werden als leeres Objekt mit trailing Slash simuliert. -- Ordner werden als leeres Objekt mit trailing Slash simuliert.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_parent_folder Übergeordneter Pfad inkl. trailing Slash (z.B. eingang/) -- Parameter: i_parent_folder Übergeordneter Pfad inkl. trailing Slash (z.B. eingang/)
-- i_folder_name Name des neuen Ordners (ohne Slash) -- i_folder_name Name des neuen Ordners (ohne Slash)
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Prozedur erstellt -- SCK 2026-04-08 Prozedur erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is
@@ -809,7 +826,7 @@ create or replace package body pck_net_storage as
if instr(i_folder_name, '/') > 0 if instr(i_folder_name, '/') > 0
then then
raise_application_error(-20011, 'Ordnername darf keinen Schrägstrich enthalten'); raise_application_error(-20011, 'Ordnername darf keinen Schrägstrich enthalten');
end if; end if;
if l_prefix is not null if l_prefix is not null
@@ -840,13 +857,13 @@ create or replace package body pck_net_storage as
function f_get_object_metadata (i_object_key in varchar2) return t_object_meta function f_get_object_metadata (i_object_key in varchar2) return t_object_meta
/*Kopf------------------------------------------------------------------------------------------------ /*Kopf------------------------------------------------------------------------------------------------
-- Beschreibung: Ruft die Metadaten eines Objekts per HEAD-Request ab (kein Download des Inhalts). -- Beschreibung: Ruft die Metadaten eines Objekts per HEAD-Request ab (kein Download des Inhalts).
-- Liest Größe, Content-Type, Last-Modified und ETag aus den Response-Headern. -- Liest Größe, Content-Type, Last-Modified und ETag aus den Response-Headern.
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Parameter: i_object_key Vollständiger Objektschlüssel im Bucket -- Parameter: i_object_key Vollständiger Objektschlüssel im Bucket
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- Rückgabe: t_object_meta Record mit object_name, object_size, last_modified, content_type, etag -- Rückgabe: t_object_meta Record mit object_name, object_size, last_modified, content_type, etag
------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------
-- MA Datum Änderung -- MA Datum Änderung
-- SCK 2026-04-08 Funktion erstellt -- SCK 2026-04-08 Funktion erstellt
------------------------------------------------------------------------------------------------Kopf*/ ------------------------------------------------------------------------------------------------Kopf*/
is is

View File

@@ -5,10 +5,10 @@ from lg_app_log
--truncate table lg_app_log; --truncate table lg_app_log;
/* /*
Test Calls für logs Test Calls für logs
*/ */
begin begin
-- Neuen Batch-Ordner für heutigen Import anlegen -- Neuen Batch-Ordner für heutigen Import anlegen
pck_net_storage.p_create_folder( pck_net_storage.p_create_folder(
i_prefix => 'testmandant-42/Eingang/Verarbeitet 2026/' i_prefix => 'testmandant-42/Eingang/Verarbeitet 2026/'
,i_folder_name => 'Batch-2026-04-09' ,i_folder_name => 'Batch-2026-04-09'
@@ -41,7 +41,7 @@ begin
,i_new_name => 'angebot_huber_2026-04.pdf' ,i_new_name => 'angebot_huber_2026-04.pdf'
); );
-- scan_003 ist ein Duplikat, wird gelöscht -- scan_003 ist ein Duplikat, wird gelöscht
pck_net_storage.p_delete_object( pck_net_storage.p_delete_object(
i_object_key => 'testmandant-42/Eingang/Import/scan_003.pdf' i_object_key => 'testmandant-42/Eingang/Import/scan_003.pdf'
); );

View File

@@ -0,0 +1,34 @@
BEGIN
null;
--pck_auto_import.p_run_ba_korrespondenz_dateieingang_automation;
/*
if pck_system.f_get_par_wert_by_programmid('AUTOMATON_API_KEY') = 'sRYh5-)j+~VY7x9A4Q6#Sz8qu7_osjTHlw94KSüJWPpsTäkrwWl5'
then dbms_output.put_line('correct');
else dbms_output.put_line('error');
end if;
*/
END;
/
select *
from LG_APP_LOG
order by log_id desc
;
DECLARE
l_response CLOB;
l_url VARCHAR2(500) := 'https://test.grafana.inkasso.ewgala.galabau.de';
BEGIN
-- Einfacher GET-Request
l_response := APEX_WEB_SERVICE.MAKE_REST_REQUEST(
p_url => l_url
,p_http_method => 'GET'
,p_wallet_path => 'file:/u01/app/oracle/product/19.0.0.0/dbhome_1/wallets/combined_wallet'
);
--DBMS_OUTPUT.PUT_LINE('Response:');
--DBMS_OUTPUT.PUT_LINE(l_response);
END;
/

View File

@@ -1,5 +1,5 @@
-- Schema-Level Type für f_list_objects Cursor-Rückgabe. -- Schema-Level Type für f_list_objects Cursor-Rückgabe.
-- Wird benötigt da Oracle TABLE() in SQL nur schema-level Types unterstützt. -- Wird benötigt da Oracle TABLE() in SQL nur schema-level Types unterstützt.
create or replace type t_net_storage_row as object ( create or replace type t_net_storage_row as object (
object_key varchar2(1024) object_key varchar2(1024)
,object_path varchar2(1024) ,object_path varchar2(1024)

View File

@@ -1,2 +1,3 @@
.env .env
target target
*private-key.pem

View File

@@ -1,3 +1,4 @@
{ {
"java.configuration.updateBuildConfiguration": "interactive" "java.configuration.updateBuildConfiguration": "disabled",
"java.compile.nullAnalysis.mode": "disabled"
} }

View File

@@ -0,0 +1,17 @@
# ---------------------------------------------------------------------------
# Build-Stage: Maven-Build
# ---------------------------------------------------------------------------
FROM eclipse-temurin:25-jdk AS builder
WORKDIR /build
COPY . .
RUN ./mvnw package -DskipTests --no-transfer-progress
# ---------------------------------------------------------------------------
# Runtime-Stage: Minimales JRE-Image
# ---------------------------------------------------------------------------
FROM eclipse-temurin:25-jre
WORKDIR /app
COPY --from=builder /build/target/quarkus-app/ ./
EXPOSE 8080
USER 1000
ENTRYPOINT ["java", "-jar", "quarkus-run.jar"]

View File

@@ -0,0 +1,10 @@
# ---------------------------------------------------------------------------
# Build- und Deploy-Konfiguration (keine Secrets — kann committet werden)
# ---------------------------------------------------------------------------
REGISTRY=ocir.eu-frankfurt-1.oci.oraclecloud.com/frhqaxi5sgcg
IMAGE_NAME=container/automaton
IMAGE_TAG=1.0.0
REGISTRY_USER=frhqaxi5sgcg/<your username> # frhqaxi5sgcg is the tenancy ID
REGISTRY_PW=<your users auth token>

View File

@@ -0,0 +1,19 @@
# Docker build (for arm nodes using qemu)
- Install docker CLI https://daniel.es/blog/how-to-install-docker-in-wsl-without-docker-desktop/
- For reference: Guide for setting up multiarch build support: https://docs.docker.com/build/building/multi-platform/
- Configure qemu emulation for arm on x86_64:
- docker buildx create --name multi-arch-builder --use
- docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- docker buildx inspect --bootstrap
- Before building and pushing: Copy the .env-example file to .env and enter auth info inside it
- run ./build.sh (executes docker buildx build command to build an image on a x86 host that can run on an arm node, e.g.:
docker buildx build --platform linux/arm64 --load -t $IMAGE . --build-arg http_proxy=$http_proxy --build-arg https_proxy=$https_proxy --build-arg no_proxy=$no_proxy)
Multiarch images cannot be loaded into the deamon using --load but need to be pushed into a registry directly using --push or be exported to a file
# Docker push image to OCI
- cd docker
- open .env and increase version number in "IMAGE_TAG=..."-variable
- run ./build.sh

View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "${SCRIPT_DIR}/.env"
IMAGE="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
echo "=== Image bauen & pushen: ${IMAGE} ==="
docker login -u $REGISTRY_USER -p $REGISTRY_PW https://$REGISTRY
docker buildx build \
--platform linux/arm64,linux/amd64 \
--push \
-t $IMAGE \
"${SCRIPT_DIR}/.."
# in case you use a proxy:
#--build-arg http_proxy=$http_proxy \
#--build-arg https_proxy=$https_proxy \
#--build-arg no_proxy=$no_proxy \
echo "Fertig: ${IMAGE}"

View File

@@ -30,16 +30,18 @@ FileProcessingPipeline [ManagedExecutor — Hintergrund-Thread]
├─→ OciUploadService.upload() [OCI SDK] ├─→ OciUploadService.upload() [OCI SDK]
│ └─ Dateien in eingang/<zip-name>/ + Marker │ └─ Dateien in eingang/<zip-name>/ + Marker
├─→ SftpService.renameRemote() [SSHJ] ├─→ SftpService.deleteRemote() [SSHJ]
│ └─ .processed (Erfolg) oder .error (Fehler) │ └─ ZIP gelöscht (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] └─→ Cleanup: lokale Dateien löschen [immer, im finally]
│ nach allen ZIPs (einmalig):
└─→ OrdsNotificationService.notify() [MicroProfile REST Client]
└─ POST pck_auto_import.p_process_incoming_ba_data
Oracle DB (pck_auto_import verarbeitet eingang/<zip-name>/) Oracle DB (pck_auto_import verarbeitet alle eingang/-Unterordner)
``` ```
## Pipeline-Steps ## Pipeline-Steps

View File

@@ -110,7 +110,7 @@ quarkus-automaton/
| `sftp-download` | `SftpService` | SSHJ | Lädt ZIP in lokales Arbeitsverzeichnis | | `sftp-download` | `SftpService` | SSHJ | Lädt ZIP in lokales Arbeitsverzeichnis |
| `zip-extract` | `ZipExtractionService` | Apache Commons Compress | Entpackt ZIP, preserviert Ordnerstruktur | | `zip-extract` | `ZipExtractionService` | Apache Commons Compress | Entpackt ZIP, preserviert Ordnerstruktur |
| `oci-upload` | `OciUploadService` | OCI SDK | Lädt Dateien + Marker zu OCI Object Storage | | `oci-upload` | `OciUploadService` | OCI SDK | Lädt Dateien + Marker zu OCI Object Storage |
| `sftp-rename` | `SftpService` | SSHJ | Remote-Rename zu `.processed` oder `.error` | | `sftp-rename` | `SftpService` | SSHJ | Remote-Rename zu `.processed` (bei Erfolg) oder `.error` (nur bei ungültiger ZIP) |
| `ords-notify` | `OrdsNotificationService` | MicroProfile REST Client | Ruft ORDS-Endpunkt auf | | `ords-notify` | `OrdsNotificationService` | MicroProfile REST Client | Ruft ORDS-Endpunkt auf |
| `cleanup` | `FileProcessingPipeline` | pure Java | Löscht lokale Arbeitsdateien (ZIP + entpackte Dateien) | | `cleanup` | `FileProcessingPipeline` | pure Java | Löscht lokale Arbeitsdateien (ZIP + entpackte Dateien) |
@@ -244,14 +244,14 @@ n8n fire-and-forget-Verhalten.
### Fehlerklassen ### Fehlerklassen
| Fehler | Typ | Retry | Verhalten | | Fehler | Typ | Umbenennung | Verhalten |
|---|---|---|---| |---|---|---|---|
| SFTP-Verbindung fehlgeschlagen | transient | nein | Nächster APEX-Lauf (1h) versucht es | | SFTP-Verbindung / Download fehlgeschlagen | transient | keine | Datei bleibt auf SFTP — nächster APEX-Lauf (1h) versucht es |
| ZIP beschädigt | persistent | nein | ZIP auf SFTP umbenennen zu `.error`, Log | | ZIP beschädigt / ungültig | persistent | `.error` | Datei ist defekt, manuelle Prüfung nötig |
| OCI-Verbindung fehlgeschlagen (z.B. 503) | transient | ja (exponential backoff) | @Retry | | OCI-Verbindung fehlgeschlagen | transient | keine | Datei bleibt auf SFTP — nächster Lauf versucht erneut (OCI PUT idempotent) |
| OCI-Upload einer Datei schlägt fehl | persistent | nein | SFTP-Rename zu `.error`, Log — bereits hochgeladene OCI-Dateien bleiben (idempotent) | | SFTP-Rename zu `.processed` fehlgeschlagen | transient | keine | ORDS wurde noch nicht aufgerufen (kommt danach) — kein Doppelimport; nächster Lauf wiederholt den Schritt |
| ORDS-Aufruf schlägt fehl | transient | ja (2-3x) | Marker liegt vor APEX Automation schlägt beim nächsten Lauf ein | | ORDS-Aufruf schlägt fehl | transient | keine (`.processed` bereits gesetzt) | Marker liegt in OCI vor APEX Automation findet ihn beim nächsten Lauf |
| Allgemein technischer Fehler | fallabhängig | siehe SmallRye Fault Tolerance | Exception-Log | | Unerwarteter Laufzeitfehler | fallabhängig | keine | Exception wird geloggt, Datei bleibt auf SFTP |
### Retry-Strategie (SmallRye Fault Tolerance) ### Retry-Strategie (SmallRye Fault Tolerance)
@@ -309,17 +309,23 @@ Credentials, Fehlerbehandlung).
Pipeline.processAll(): Pipeline.processAll():
1. SftpService.listZipFiles() → ["export_2026-04-08.zip", ...] 1. SftpService.listZipFiles() → ["export_2026-04-08.zip", ...]
2. für jede ZIP: 2. für jede ZIP:
a. SftpService.download(zip) → lokale Datei a. SftpService.download(zip) → lokale Datei
b. ZipExtractionService.extract() → ProcessingContext mit FileEntry-Liste b. ZipExtractionService.extract() → ProcessingContext mit FileEntry-Liste
c. OciUploadService.upload() → Dateien + Marker in OCI ↳ ZipException → Rename zu .error, Abbruch
d. SftpService.renameRemote(.processed oder .error) c. OciUploadService.uploadFiles() → Dateien in OCI (noch kein Marker)
e. OrdsNotificationService.notify() d. SftpService.renameRemote(.processed)
e. OciUploadService.uploadMarker() → Marker in OCI (erst nach Rename — siehe Invariante)
f. OrdsNotificationService.notify()
f. cleanup: lokale ZIP + Entpack-Verzeichnis löschen ← immer, auch bei Fehler f. cleanup: lokale ZIP + Entpack-Verzeichnis löschen ← immer, auch bei Fehler
``` ```
**Cleanup (Schritt f) läuft immer** — in einem `finally`-Block — damit kein Disk-Vollaufen **Cleanup (Schritt f) läuft immer** — in einem `finally`-Block — damit kein Disk-Vollaufen
bei Fehlern oder großen ZIPs. bei Fehlern oder großen ZIPs.
**Umbenennung zu `.error`** erfolgt ausschließlich bei `ZipException` (defekte/ungültige Datei).
Bei Infrastrukturfehlern (SFTP, OCI, ORDS) bleibt die Datei unverändert auf dem SFTP und wird
beim nächsten Lauf automatisch erneut verarbeitet.
--- ---
## OCI-Authentifizierung (SimpleAuthenticationDetailsProvider) ## OCI-Authentifizierung (SimpleAuthenticationDetailsProvider)
@@ -343,11 +349,13 @@ public class OciUploadService {
.tenantId(config.tenancyId()) .tenantId(config.tenancyId())
.userId(config.userId()) .userId(config.userId())
.fingerprint(config.fingerprint()) .fingerprint(config.fingerprint())
.region(Region.fromRegionId(config.region())) .privateKeySupplier(() -> Files.newInputStream(Path.of(config.privateKeyPath())))
.privateKeySupplier(new FilePrivateKeySupplier(config.privateKeyPath()))
.build(); .build();
this.client = ObjectStorageClient.builder().build(auth); // Endpoint explizit setzen — verhindert blockierenden HTTP-Discovery-Call im SDK
client = ObjectStorageClient.builder()
.endpoint("https://objectstorage." + config.region() + ".oraclecloud.com")
.build(auth);
} }
} }
``` ```
@@ -452,6 +460,12 @@ public class ProcessIncomingRequest {
<artifactId>oci-java-sdk-objectstorage</artifactId> <artifactId>oci-java-sdk-objectstorage</artifactId>
<version>3.44.0</version> <version>3.44.0</version>
</dependency> </dependency>
<!-- HTTP-Provider für OCI SDK (jersey3 = Jakarta EE 9+, kompatibel mit Quarkus) -->
<dependency>
<groupId>com.oracle.oci.sdk</groupId>
<artifactId>oci-java-sdk-common-httpclient-jersey3</artifactId>
<version>3.44.0</version>
</dependency>
<!-- ZIP --> <!-- ZIP -->
<dependency> <dependency>

View File

@@ -75,11 +75,11 @@
<version>0.38.0</version> <version>0.38.0</version>
</dependency> </dependency>
<!-- OCI Object Storage SDK --> <!-- OCI Object Storage SDK — Shaded Full JAR: Jersey und alle internen Abhängigkeiten sind unter
<!-- Aktuelle Version: https://mvnrepository.com/artifact/com.oracle.oci.sdk/oci-java-sdk-objectstorage --> shaded.com.oracle.oci.javasdk.* relokiert, sodass Quarkus RESTEasy die OCI-Provider nicht scannt -->
<dependency> <dependency>
<groupId>com.oracle.oci.sdk</groupId> <groupId>com.oracle.oci.sdk</groupId>
<artifactId>oci-java-sdk-objectstorage</artifactId> <artifactId>oci-java-sdk-shaded-full</artifactId>
<version>3.44.0</version> <version>3.44.0</version>
</dependency> </dependency>

View File

@@ -18,7 +18,7 @@ import java.util.Map;
* REST-Endpunkt für den Dateieingang-Trigger. * REST-Endpunkt für den Dateieingang-Trigger.
* Wird von der APEX Automation stündlich per HTTP POST aufgerufen (fire & forget). * Wird von der APEX Automation stündlich per HTTP POST aufgerufen (fire & forget).
*/ */
@Path("/api/process-incoming") @Path("/api/process-incoming-ba-korrespondenz")
@ApplicationScoped @ApplicationScoped
public class FileProcessingResource { public class FileProcessingResource {
@@ -38,11 +38,15 @@ public class FileProcessingResource {
@POST @POST
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response triggerProcessing(@HeaderParam("X-Api-Key") String apiKey) { public Response triggerProcessing(@HeaderParam("X-Api-Key") String apiKey) {
//Log.infof("API-key correct: %s", config.api().key());
//Log.infof("API-key received: %s", apiKey);
if (apiKey == null || !config.api().key().equals(apiKey)) { if (apiKey == null || !config.api().key().equals(apiKey)) {
Log.warn("Trigger abgelehnt — ungültiger oder fehlender API-Key"); Log.warnf("Trigger abgelehnt — ungültiger oder fehlender API-Key. Key: %s", apiKey);
return Response.status(Response.Status.UNAUTHORIZED).build(); return Response.status(Response.Status.UNAUTHORIZED).build();
} }
Log.info("API-Key valide, Pipeline-Trigger wird verarbeitet");
boolean started = pipeline.tryProcessAllAsync(); boolean started = pipeline.tryProcessAllAsync();
if (!started) { if (!started) {

View File

@@ -0,0 +1,86 @@
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 testmandant-42/}.
* Muss mit {@code /} enden.
*/
String tenantPrefix();
/**
* Gemeinsamer Basis-Prefix für alle BA-Eingangs-Pfade unterhalb von {@code tenantPrefix},
* z.B. {@code BA/Eingang/}. Muss mit {@code /} enden.
*/
String baBasePrefix();
/** Konfiguration für die BA-Korrespondenzen-Pipeline. */
Korrespondenzen korrespondenzen();
/** Konfiguration für die BA-Aufrechnungen-Pipeline. */
Aufrechnungen aufrechnungen();
/** 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();
/**
* Dateiname des DB-Processing-Markers, der nach dem Upload aller Nutzdateien in OCI abgelegt wird.
* Default: {@code _READY_FOR_DB_PROCESSING_}.
* Muss mit der APEX Automation und dem ORDS-Package abgestimmt sein.
*/
String markerFilenameDbProcessing();
interface Korrespondenzen {
/**
* Prefix für eingehende Korrespondenz-Dateien relativ zu {@code baBasePrefix},
* z.B. {@code Import/BA-Korrespondenzen/}. Muss mit {@code /} enden.
* Vollständiger Pfad: {@code tenantPrefix + baBasePrefix + incomingPrefix}.
*/
String incomingPrefix();
/**
* Prefix für archivierte ZIP-Originaldateien relativ zu {@code baBasePrefix},
* z.B. {@code BA-Korrespondenzen ZIP-Dateien}. Kein abschließendes {@code /} —
* das aktuelle Jahr wird zur Laufzeit angehängt: {@code <prefix> <yyyy>/}.
* Vollständiger Pfad: {@code tenantPrefix + baBasePrefix + archivePrefix + " 2026/"}.
*/
String archivePrefix();
}
interface Aufrechnungen {
/**
* Prefix für eingehende Aufrechnungs-Dateien relativ zu {@code baBasePrefix},
* z.B. {@code Import/Aufrechnungen/}. Muss mit {@code /} enden.
* Vollständiger Pfad: {@code tenantPrefix + baBasePrefix + incomingPrefix}.
*/
String incomingPrefix();
}
}

View File

@@ -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();
}

View File

@@ -1,4 +1,4 @@
package de.galabau.dateieingang.sftp; package de.galabau.dateieingang.config;
import io.smallrye.config.ConfigMapping; import io.smallrye.config.ConfigMapping;

View File

@@ -12,8 +12,8 @@ import java.util.UUID;
*/ */
public class ProcessingContext { public class ProcessingContext {
/** Eindeutige Lauf-ID — wird als MDC-Feld {@code runId} gesetzt. */ /** Eindeutige Datei-ID — wird als MDC-Feld {@code fileId} gesetzt. */
public final UUID runId; public final UUID fileId;
/** Originaler ZIP-Dateiname auf dem SFTP-Server, z.B. {@code export_2026-04-08.zip}. */ /** Originaler ZIP-Dateiname auf dem SFTP-Server, z.B. {@code export_2026-04-08.zip}. */
public final String zipFilename; public final String zipFilename;
@@ -39,8 +39,8 @@ public class ProcessingContext {
/** Aktueller Verarbeitungsstatus. */ /** Aktueller Verarbeitungsstatus. */
public ProcessingStatus status = ProcessingStatus.PENDING; public ProcessingStatus status = ProcessingStatus.PENDING;
public ProcessingContext(UUID runId, String zipFilename) { public ProcessingContext(UUID fileId, String zipFilename) {
this.runId = runId; this.fileId = fileId;
this.zipFilename = zipFilename; this.zipFilename = zipFilename;
this.zipNameWithoutExt = zipFilename.endsWith(".zip") this.zipNameWithoutExt = zipFilename.endsWith(".zip")
? zipFilename.substring(0, zipFilename.length() - 4) ? zipFilename.substring(0, zipFilename.length() - 4)

View File

@@ -5,6 +5,7 @@ public enum ProcessingStatus {
PENDING, PENDING,
PARTIALLY_UPLOADED, PARTIALLY_UPLOADED,
MARKER_UPLOADED, MARKER_UPLOADED,
// TODO: ORDS_NOTIFIED wird seit dem Refactoring (ORDS-Aufruf einmalig am Ende der Pipeline, nicht mehr pro ZIP) nicht mehr gesetzt — entfernen
ORDS_NOTIFIED, ORDS_NOTIFIED,
FAILED FAILED
} }

View File

@@ -1,32 +1,162 @@
package de.galabau.dateieingang.oci; package de.galabau.dateieingang.oci;
import com.oracle.bmc.Region;
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.exception.OciException;
import de.galabau.dateieingang.model.FileEntry;
import de.galabau.dateieingang.model.ProcessingContext; import de.galabau.dateieingang.model.ProcessingContext;
import de.galabau.dateieingang.model.ProcessingStatus;
import io.quarkus.logging.Log; import io.quarkus.logging.Log;
//import io.quarkus.runtime.Startup;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped; 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.time.Year;
import java.util.List;
/** /**
* Lädt die entpackten Dateien und den Marker in OCI Object Storage hoch. * Lädt die entpackten Dateien und den Marker in OCI Object Storage hoch.
* * Authentifizierung via OCI HTTP Signature V1 (entspricht APEX Web Credential vom Typ OCI).
* <p><b>Stub:</b> 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.
*/ */
//@Startup
@ApplicationScoped @ApplicationScoped
public class OciUploadService { public class OciUploadService {
@Inject
OciConfig config;
private ObjectStorage client;
@PostConstruct
void init() {
Log.info("Initialisiere OCI ObjectStorage-Client...");
try {
SimpleAuthenticationDetailsProvider auth = SimpleAuthenticationDetailsProvider.builder()
.tenantId(config.tenancyId())
.userId(config.userId())
.fingerprint(config.fingerprint())
.region(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();
Log.info("Authentifizierung...");
client = ObjectStorageClient.builder()
.build(auth);
Log.infof("OCI ObjectStorage-Client initialisiert (Region: %s, Bucket: %s)", config.region(), config.bucket());
} catch (Throwable e) {
Log.errorf(e, "OCI ObjectStorage-Client Initialisierung fehlgeschlagen");
throw new RuntimeException("OCI-Client konnte nicht initialisiert werden", e);
}
}
/** /**
* Lädt alle Dateien aus {@code context.extractedFiles} sowie den Marker in OCI hoch. * Lädt alle Nutzdateien aus {@code context.extractedFiles} in OCI hoch — ohne Marker.
* Der Marker wird erst nach dem SFTP-Rename zu {@code .processed} gesetzt (siehe
* {@link #uploadMarker}), damit APEX Automation den Batch nie verarbeitet bevor die
* ZIP-Datei auf dem SFTP als verarbeitet markiert ist.
* *
* @param context enthält die Liste der hochzuladenden Dateien und den Ziel-Prefix * @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 { public void uploadFiles(ProcessingContext context) throws OciException {
// TODO: OCI-Upload implementieren (OCI SDK, SimpleAuthenticationDetailsProvider) List<FileEntry> files = context.extractedFiles.stream()
Log.infof("[STUB] OCI-Upload übersprungen für '%s' (%d Dateien) — wird später implementiert", .filter(e -> !e.isMarker)
context.zipNameWithoutExt, context.extractedFiles.size()); .toList();
Log.infof("OCI-Upload: %d Datei(en) für '%s'", files.size(), context.zipNameWithoutExt);
for (FileEntry entry : files) {
String key = buildKorrespondenzenKey(context.zipNameWithoutExt, entry.relativePath);
entry.ociKey = key;
putFile(key, context.localExtractDir.resolve(entry.relativePath), entry.fileSize);
Log.infof("Datei hochgeladen: %s (%d Bytes)", key, entry.fileSize);
}
Log.infof("OCI-Upload Dateien abgeschlossen: %d Datei(en) in '%s'",
files.size(), buildKorrespondenzenPrefix(context.zipNameWithoutExt));
}
/**
* Setzt den Marker in OCI — signalisiert der DB-Verarbeitung, dass der Batch vollständig ist.
* Wird erst nach dem SFTP-Delete aufgerufen, damit Marker und
* SFTP-Zustand immer konsistent sind: Marker vorhanden ↔ ZIP bereits vom SFTP gelöscht.
*
* @param context enthält den Ziel-Prefix für den Marker-Key
* @throws OciException bei Verbindungs- oder Upload-Fehlern
*/
public void uploadMarker(ProcessingContext context) throws OciException {
String markerKey = buildKorrespondenzenKey(context.zipNameWithoutExt, config.markerFilenameDbProcessing());
Log.infof("Lade Marker hoch: '%s'", markerKey);
putMarker(markerKey);
context.markerUploaded = true; context.markerUploaded = true;
context.status = ProcessingStatus.MARKER_UPLOADED; Log.infof("Marker hochgeladen: '%s'", markerKey);
}
/**
* Lädt die Original-ZIP-Datei in den Archivordner in OCI hoch.
* Ziel-Key: {@code tenantPrefix + baBasePrefix + archivePrefix + " <Jahr>/" + zipFilename}
*
* @param context enthält den lokalen ZIP-Pfad und den Dateinamen
* @throws OciException bei Verbindungs- oder Upload-Fehlern
* @throws IOException bei Problemen beim Lesen der lokalen ZIP-Datei
*/
public void uploadZipFile(ProcessingContext context) throws OciException, IOException {
String yearFolder = config.korrespondenzen().archivePrefix() + " " + Year.now().getValue() + "/";
String key = config.tenantPrefix() + config.baBasePrefix() + yearFolder + context.zipFilename;
long fileSize = Files.size(context.localZipPath);
Log.infof("Lade ZIP-Archiv hoch: '%s' (%d Bytes)", key, fileSize);
putFile(key, context.localZipPath, fileSize);
Log.infof("ZIP-Archiv hochgeladen: '%s'", key);
}
private String buildKorrespondenzenPrefix(String zipNameWithoutExt) {
return config.tenantPrefix() + config.baBasePrefix()
+ config.korrespondenzen().incomingPrefix() + zipNameWithoutExt + "/";
}
private String buildKorrespondenzenKey(String zipNameWithoutExt, String relativePath) {
return buildKorrespondenzenPrefix(zipNameWithoutExt) + relativePath;
}
private void putFile(String key, Path localFile, long fileSize) throws OciException {
try (InputStream is = Files.newInputStream(localFile)) {
client.putObject(PutObjectRequest.builder()
.namespaceName(config.namespace())
.bucketName(config.bucket())
.objectName(key)
.putObjectBody(is)
.contentLength(fileSize)
.build());
} catch (Exception e) {
throw new OciException("OCI-Upload fehlgeschlagen für '" + key + "'", e);
}
}
private void putMarker(String key) throws OciException {
try (InputStream is = InputStream.nullInputStream()) {
client.putObject(PutObjectRequest.builder()
.namespaceName(config.namespace())
.bucketName(config.bucket())
.objectName(key)
.putObjectBody(is)
.contentLength(0L)
.build());
} catch (Exception e) {
throw new OciException("OCI-Upload Marker fehlgeschlagen für '" + key + "'", e);
}
} }
} }

View File

@@ -0,0 +1,30 @@
package de.galabau.dateieingang.ords;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
/**
* MicroProfile REST Client für den ORDS-Endpunkt des Dateieingang-Service.
* Base-URL wird über {@code quarkus.rest-client.ords-client.url} konfiguriert
* und zeigt auf den ORDS-Modul-Pfad (z.B. {@code .../ords/myschema/auto_import}).
* Weitere Endpunkte zukünftiger Pipelines werden hier als neue Methoden ergänzt.
*/
@RegisterRestClient(configKey = "ords-client")
@RegisterProvider(OrdsLoggingFilter.class)
public interface OrdsClient {
/**
* Ruft {@code pck_auto_import.p_process_incoming_ba_data} über ORDS auf.
* Die Prozedur verarbeitet alle offenen OCI-Batches (Unterordner mit Marker).
*
* @param apiKey API-Key aus {@code galabau.ords.api-key} (Header: {@code X-Api-Key})
* @return ORDS-Response (erwartet: 2xx)
*/
@POST
@Path("/process_incoming_ba_data")
Response processIncomingBaData(@HeaderParam("X-Api-Key") String apiKey);
}

View File

@@ -0,0 +1,54 @@
package de.galabau.dateieingang.ords;
import io.quarkus.logging.Log;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.client.ClientResponseContext;
import jakarta.ws.rs.client.ClientResponseFilter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
/** Loggt ORDS-Requests und -Responses im selben Format wie der DefaultClientLogger, maskiert jedoch den X-Api-Key-Header. */
public class OrdsLoggingFilter implements ClientRequestFilter, ClientResponseFilter {
@Override
public void filter(ClientRequestContext ctx) {
String headers = ctx.getHeaders().entrySet().stream()
.map(e -> e.getKey() + "=" + (
"X-Api-Key".equals(e.getKey())
? mask(String.valueOf(e.getValue().getFirst()))
: e.getValue().getFirst()
))
.collect(Collectors.joining(" "));
String body = ctx.hasEntity() ? "<body>" : "Empty body";
Log.infof("Request: %s %s Headers[%s], %s", ctx.getMethod(), ctx.getUri(), headers, body);
}
@Override
public void filter(ClientRequestContext req, ClientResponseContext res) throws IOException {
String headers = res.getHeaders().entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue().getFirst())
.collect(Collectors.joining(" "));
String body = "";
if (res.hasEntity()) {
byte[] bytes = res.getEntityStream().readAllBytes();
body = new String(bytes, StandardCharsets.UTF_8);
res.setEntityStream(new ByteArrayInputStream(bytes));
}
Log.infof("Response: %s %s, Status[%d %s], Headers[%s], Body:\n%s",
req.getMethod(), req.getUri(),
res.getStatus(), res.getStatusInfo().getReasonPhrase(),
headers, body);
}
private static String mask(String value) {
return value.substring(0, Math.min(4, value.length())) + "***";
}
}

View File

@@ -1,30 +1,58 @@
package de.galabau.dateieingang.ords; package de.galabau.dateieingang.ords;
import de.galabau.dateieingang.config.OrdsConfig;
import de.galabau.dateieingang.exception.OrdsException; import de.galabau.dateieingang.exception.OrdsException;
import de.galabau.dateieingang.model.ProcessingContext;
import io.quarkus.logging.Log; import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.faulttolerance.Retry;
import org.eclipse.microprofile.faulttolerance.Timeout;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import java.time.temporal.ChronoUnit;
/** /**
* Benachrichtigt den ORDS-Endpunkt {@code pck_auto_import.p_process_incoming_files}, * Benachrichtigt den ORDS-Endpunkt {@code pck_auto_import.p_process_incoming_ba_data},
* damit die DB-Verarbeitung sofort angestoßen wird. * damit die DB-Verarbeitung sofort angestoßen wird.
* *
* <p><b>Stub:</b> ORDS-Aufruf ist noch nicht implementiert. * <p>Bei Ausfall ist die Verarbeitung durch die APEX Automation abgesichert:
* Bei einem Ausfall wäre die Verarbeitung ohnehin durch die APEX Automation abgesichert * Sie findet den Marker beim nächsten Stundenlauf und ruft die Prozedur selbst auf.
* (diese findet den Marker beim nächsten Stundenlauf).
*/ */
@ApplicationScoped @ApplicationScoped
public class OrdsNotificationService { public class OrdsNotificationService {
@Inject
@RestClient
OrdsClient ordsClient;
@Inject
OrdsConfig config;
/** /**
* Sendet eine Benachrichtigung an den ORDS-Endpunkt. * Löst die DB-Verarbeitung via ORDS aus ({@code pck_auto_import.p_process_incoming_ba_data}).
* Wird bei transienten Fehlern bis zu 3-mal wiederholt (1s Backoff, 10s Timeout je Versuch).
* Maximale Wartezeit: ca. 33 Sekunden (3 × 10s + 3 × 1s Backoff).
* *
* @param context enthält {@code zipNameWithoutExt} und {@code runId} für den Request-Body
* @throws OrdsException wenn der ORDS-Aufruf nach allen Retries fehlschlägt * @throws OrdsException wenn der ORDS-Aufruf nach allen Retries fehlschlägt
*/ */
public void notify(ProcessingContext context) throws OrdsException { @Retry(maxRetries = 3, delay = 1000, delayUnit = ChronoUnit.MILLIS,
// TODO: ORDS REST-Client implementieren (MicroProfile REST Client + @Retry) retryOn = OrdsException.class)
Log.infof("[STUB] ORDS-Benachrichtigung übersprungen für '%s' — wird später implementiert", @Timeout(value = 10, unit = ChronoUnit.SECONDS)
context.zipNameWithoutExt); public void triggerDbProcessing() throws OrdsException {
Log.info("Rufe ORDS-Endpunkt auf");
Response response;
try {
response = ordsClient.processIncomingBaData(config.apiKey());
} catch (Exception e) {
throw new OrdsException("ORDS-Verbindung fehlgeschlagen", e);
}
int status = response.getStatus();
if (status >= 400) {
throw new OrdsException("ORDS antwortete mit HTTP " + status);
}
Log.infof("ORDS-Endpunkt aufgerufen, HTTP %d", status);
} }
} }

View File

@@ -1,5 +1,6 @@
package de.galabau.dateieingang.pipeline; package de.galabau.dateieingang.pipeline;
import de.galabau.dateieingang.config.SftpConfig;
import de.galabau.dateieingang.exception.OciException; import de.galabau.dateieingang.exception.OciException;
import de.galabau.dateieingang.exception.OrdsException; import de.galabau.dateieingang.exception.OrdsException;
import de.galabau.dateieingang.exception.SftpException; import de.galabau.dateieingang.exception.SftpException;
@@ -19,6 +20,8 @@ import org.slf4j.MDC;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -44,6 +47,9 @@ public class FileProcessingPipeline {
@Inject @Inject
OrdsNotificationService ordsNotificationService; OrdsNotificationService ordsNotificationService;
@Inject
SftpConfig sftpConfig;
@Inject @Inject
ManagedExecutor executor; ManagedExecutor executor;
@@ -63,6 +69,8 @@ public class FileProcessingPipeline {
executor.submit(() -> { executor.submit(() -> {
try { try {
processAll(); processAll();
} catch (Throwable e) { // nicht exception catchen, weil Error in OCI SDK auftreten können, die Throwable aber nicht Excption sind. Die würden sonst nicht geloggt
Log.errorf(e, "Unerwarteter Fehler im Pipeline-Lauf");
} finally { } finally {
isRunning.set(false); isRunning.set(false);
} }
@@ -70,35 +78,50 @@ public class FileProcessingPipeline {
return true; return true;
} }
void processAll() { private void processAll() {
Log.info("Pipeline-Lauf gestartet"); UUID pipelineRunId = UUID.randomUUID();
MDC.put("pipelineRunId", pipelineRunId.toString());
Log.infof("Pipeline-Lauf gestartet [pipelineRunId=%s]", pipelineRunId);
List<String> zipFiles;
try { try {
zipFiles = sftpService.listZipFiles(); preProcessingCleanup();
} catch (SftpException e) {
Log.errorf(e, "SFTP-Listing fehlgeschlagen — Pipeline-Lauf abgebrochen"); List<String> zipFiles;
return; try {
zipFiles = sftpService.listZipFiles();
} catch (SftpException e) {
Log.errorf(e, "SFTP-Listing fehlgeschlagen — Pipeline-Lauf abgebrochen");
return;
}
if (zipFiles.isEmpty()) {
Log.info("Keine neuen ZIP-Dateien auf dem SFTP-Server gefunden");
return;
}
Log.infof("%d neue ZIP-Datei(en) auf dem SFTP-Server gefunden", zipFiles.size());
for (String zipFilename : zipFiles) {
Log.infof("ZIP-Datei gefunden: %s", zipFilename);
processZip(zipFilename);
}
MDC.put("step", "ords-notify");
try {
ordsNotificationService.triggerDbProcessing();
} catch (OrdsException e) {
Log.errorf(e, "ORDS-Benachrichtigung fehlgeschlagen — DB-Verarbeitung wird beim nächsten Lauf ausgelöst");
}
} finally {
Log.infof("Pipeline-Lauf abgeschlossen [pipelineRunId=%s]", pipelineRunId);
MDC.clear();
} }
if (zipFiles.isEmpty()) {
Log.info("Keine neuen ZIP-Dateien auf dem SFTP-Server gefunden");
return;
}
Log.infof("%d neue ZIP-Datei(en) auf dem SFTP-Server gefunden", zipFiles.size());
for (String zipFilename : zipFiles) {
Log.infof("Datei gefunden: %s", zipFilename);
processZip(zipFilename);
}
Log.info("Pipeline-Lauf abgeschlossen");
} }
private void processZip(String zipFilename) { private void processZip(String zipFilename) {
ProcessingContext context = new ProcessingContext(UUID.randomUUID(), zipFilename); ProcessingContext context = new ProcessingContext(UUID.randomUUID(), zipFilename);
MDC.put("runId", context.runId.toString()); MDC.put("fileId", context.fileId.toString());
Log.infof("Starte Verarbeitung von '%s' [fileId=%s]", zipFilename, context.fileId);
try { try {
// --- Download --- // --- Download ---
@@ -107,66 +130,134 @@ public class FileProcessingPipeline {
Log.infof("ZIP '%s' heruntergeladen (%d Bytes)", zipFilename, Log.infof("ZIP '%s' heruntergeladen (%d Bytes)", zipFilename,
Files.size(context.localZipPath)); Files.size(context.localZipPath));
// --- OCI ZIP-Archiv ---
MDC.put("step", "oci-zip-archive");
Log.info("Starte ZIP-Upload in OCI");
ociUploadService.uploadZipFile(context);
// --- Entpacken --- // --- Entpacken ---
MDC.put("step", "zip-extract"); MDC.put("step", "zip-extract");
zipExtractionService.extract(context); zipExtractionService.extract(context);
Log.infof("ZIP '%s' entpackt: %d Datei(en)", zipFilename, Log.infof("ZIP '%s' entpackt: %d Datei(en)", zipFilename,
context.extractedFiles.size()); context.extractedFiles.size());
// --- OCI Upload (Stub) --- // --- OCI Upload (Dateien, noch kein Marker) ---
MDC.put("step", "oci-upload"); MDC.put("step", "oci-upload");
ociUploadService.upload(context); context.status = ProcessingStatus.PARTIALLY_UPLOADED;
Log.info("Starte OCI-Upload");
ociUploadService.uploadFiles(context);
// --- SFTP Rename → .processed --- // --- SFTP Delete ---
MDC.put("step", "sftp-rename"); // Erst nach erfolgreichem Datei-Upload — Marker kommt danach,
sftpService.renameRemote(zipFilename, zipFilename + ".processed"); // damit Marker-Präsenz in OCI ↔ ZIP bereits vom SFTP gelöscht ist.
Log.infof("SFTP Rename: '%s' → '%s.processed'", zipFilename, zipFilename); MDC.put("step", "sftp-delete");
sftpService.deleteFile(zipFilename);
// --- ORDS Notify (Stub) --- // --- OCI Marker ---
MDC.put("step", "ords-notify"); // Signalisiert der DB-Verarbeitung, dass der Batch vollständig hochgeladen ist.
ordsNotificationService.notify(context); MDC.put("step", "oci-marker");
ociUploadService.uploadMarker(context);
context.status = ProcessingStatus.MARKER_UPLOADED;
Log.infof("Verarbeitung erfolgreich abgeschlossen: '%s'", zipFilename);
context.status = ProcessingStatus.ORDS_NOTIFIED; } catch (ZipException e) {
Log.errorf(e, "Ungültige ZIP-Datei '%s' — wird zu .error umbenannt", zipFilename);
} catch (SftpException | ZipException | OciException | OrdsException e) {
Log.errorf(e, "Verarbeitung von '%s' fehlgeschlagen: %s", zipFilename, e.getMessage());
context.status = ProcessingStatus.FAILED; context.status = ProcessingStatus.FAILED;
tryRenameToError(zipFilename); tryRenameToError(zipFilename);
} catch (SftpException | OciException e) {
Log.errorf(e, "Verarbeitung von '%s' fehlgeschlagen (Infrastruktur): %s", zipFilename, e.getMessage());
context.status = ProcessingStatus.FAILED;
} catch (IOException e) { } catch (IOException e) {
Log.errorf(e, "I/O-Fehler bei der Verarbeitung von '%s'", zipFilename); Log.errorf(e, "I/O-Fehler bei der Verarbeitung von '%s'", zipFilename);
context.status = ProcessingStatus.FAILED; context.status = ProcessingStatus.FAILED;
tryRenameToError(zipFilename); } catch (RuntimeException e) {
Log.errorf(e, "Unerwarteter Laufzeitfehler bei der Verarbeitung von '%s'", zipFilename);
context.status = ProcessingStatus.FAILED;
} finally { } finally {
cleanup(context); postProcessingCleanup(context);
MDC.clear(); long duration = Duration.between(context.startTime, LocalDateTime.now()).toMillis();
Log.infof("Datei %s abgeschlossen — Status: %s, Dauer: %dms [fileId=%s]",
zipFilename, context.status, duration, context.fileId);
Log.info("-----------------------------------------------------------------------------------------------------");
MDC.remove("fileId");
MDC.remove("step");
} }
} }
private void tryRenameToError(String zipFilename) { private void tryRenameToError(String zipFilename) {
try { try {
MDC.put("step", "sftp-rename"); MDC.put("step", "sftp-rename");
sftpService.renameRemote(zipFilename, zipFilename + ".error"); sftpService.renameFile(zipFilename, zipFilename + ".error");
Log.infof("SFTP Rename: '%s' → '%s.error'", zipFilename, zipFilename);
} catch (SftpException e) { } catch (SftpException e) {
Log.warnf(e, "Umbenennen zu .error fehlgeschlagen für '%s' — Datei bleibt auf SFTP zur manuellen Prüfung", Log.warnf(e, "Umbenennen zu .error fehlgeschlagen für '%s' — Datei bleibt auf SFTP zur manuellen Prüfung",
zipFilename); zipFilename);
} }
} }
private void cleanup(ProcessingContext context) { /**
MDC.put("step", "cleanup"); * Bereinigt verwaiste lokale Arbeitsdateien aus fehlgeschlagenen Vorläufen.
*
* <p>Wird einmal pro Pipeline-Lauf <em>vor</em> dem SFTP-Listing aufgerufen.
* Notwendig weil {@link #postProcessingCleanup} zwar im {@code finally}-Block läuft,
* aber bei I/O-Fehlern selbst fehlschlagen kann — in diesem Fall bleiben ZIP-Dateien
* und Entpack-Verzeichnisse im Arbeitsverzeichnis zurück. Ohne diesen Schritt würden
* sich diese Reste akkumulieren und das Arbeitsverzeichnis über Zeit vollschreiben.
*
* <p>Ein Zeitstempel-Schwellwert ist nicht nötig: der {@code AtomicBoolean}-Guard in
* {@link #tryProcessAllAsync} stellt sicher dass nie zwei Läufe gleichzeitig aktiv sind.
* Alles was beim Start eines Laufs im Arbeitsverzeichnis liegt, ist daher garantiert
* ein Überrest eines abgeschlossenen oder abgebrochenen Vorlaufs.
*/
private void preProcessingCleanup() {
MDC.put("step", "pre-cleanup");
Path workDir = Path.of(sftpConfig.localWorkDir());
if (!Files.exists(workDir)) {
return;
}
try (Stream<Path> entries = Files.list(workDir)) {
entries.forEach(path -> {
try {
if (Files.isDirectory(path)) {
deleteLocalDirectory(path);
Log.warnf("Verwaistes Entpack-Verzeichnis gelöscht: %s", path);
} else {
Files.delete(path);
Log.warnf("Verwaiste Datei gelöscht: %s", path);
}
} catch (IOException e) {
Log.warnf(e, "Pre-Cleanup fehlgeschlagen für: %s", path);
}
});
} catch (IOException e) {
Log.warnf(e, "Pre-Cleanup: Arbeitsverzeichnis konnte nicht gelesen werden: %s", workDir);
}
}
/**
* Bereinigt die lokalen Arbeitsdateien eines abgeschlossenen Laufs (ZIP + Entpack-Verzeichnis).
*
* <p>Wird im {@code finally}-Block von {@link #processZip} aufgerufen, also sowohl nach
* erfolgreichem Abschluss als auch nach Fehlern. Schlägt dieser Cleanup bei I/O-Problemen
* fehl, verbleiben die Dateien im Arbeitsverzeichnis — sie werden dann beim nächsten
* Pipeline-Lauf durch {@link #preProcessingCleanup} entfernt.
*
* @param context enthält die Pfade der zu löschenden lokalen ZIP und des Entpack-Verzeichnisses
*/
private void postProcessingCleanup(ProcessingContext context) {
MDC.put("step", "post-cleanup");
Log.infof("Cleanup gestartet für Datei '%s'", context.zipFilename);
try { try {
if (context.localZipPath != null) { if (context.localZipPath != null) {
Files.deleteIfExists(context.localZipPath); Files.deleteIfExists(context.localZipPath);
Log.debugf("Lokale ZIP gelöscht: %s", context.localZipPath); Log.infof("Lokale ZIP gelöscht: %s", context.localZipPath);
} }
if (context.localExtractDir != null) { if (context.localExtractDir != null) {
deleteLocalDirectory(context.localExtractDir); deleteLocalDirectory(context.localExtractDir);
Log.debugf("Lokales Entpack-Verzeichnis gelöscht: %s", context.localExtractDir); Log.infof("Lokales Entpack-Verzeichnis gelöscht: %s", context.localExtractDir);
} }
} catch (IOException e) { } catch (IOException e) {
Log.warnf(e, "Cleanup für Lauf %s fehlgeschlagen — lokale Dateien verbleiben ggf. in %s", Log.warnf(e, "Cleanup für '%s' fehlgeschlagen — lokale Dateien verbleiben ggf. in %s",
context.runId, context.zipFilename,
context.localZipPath != null ? context.localZipPath.getParent() : "unbekannt"); context.localZipPath != null ? context.localZipPath.getParent() : "unbekannt");
} }
} }

View File

@@ -1,5 +1,6 @@
package de.galabau.dateieingang.sftp; package de.galabau.dateieingang.sftp;
import de.galabau.dateieingang.config.SftpConfig;
import de.galabau.dateieingang.exception.SftpException; import de.galabau.dateieingang.exception.SftpException;
import io.quarkus.logging.Log; import io.quarkus.logging.Log;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
@@ -8,7 +9,6 @@ import jakarta.inject.Inject;
import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.sftp.RemoteResourceInfo; import net.schmizz.sshj.sftp.RemoteResourceInfo;
import net.schmizz.sshj.sftp.SFTPClient; import net.schmizz.sshj.sftp.SFTPClient;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@@ -26,6 +26,7 @@ public class SftpService {
void init() { void init() {
try { try {
Files.createDirectories(Path.of(config.localWorkDir())); Files.createDirectories(Path.of(config.localWorkDir()));
Log.infof("Lokales Arbeitsverzeichnis: %s", config.localWorkDir());
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Lokales Arbeitsverzeichnis konnte nicht erstellt werden: " throw new RuntimeException("Lokales Arbeitsverzeichnis konnte nicht erstellt werden: "
+ config.localWorkDir(), e); + config.localWorkDir(), e);
@@ -44,8 +45,10 @@ public class SftpService {
private <T> T withSftp(SftpOperation<T> operation) throws SftpException { private <T> T withSftp(SftpOperation<T> operation) throws SftpException {
try (SSHClient ssh = new SSHClient()) { try (SSHClient ssh = new SSHClient()) {
configureHostKeyVerification(ssh); configureHostKeyVerification(ssh);
Log.infof("Verbinde zu SFTP %s:%d", config.host(), config.port());
ssh.connect(config.host(), config.port()); ssh.connect(config.host(), config.port());
authenticate(ssh); authenticate(ssh);
Log.infof("SFTP-Verbindung hergestellt");
try (SFTPClient sftp = ssh.newSFTPClient()) { try (SFTPClient sftp = ssh.newSFTPClient()) {
return operation.execute(sftp); return operation.execute(sftp);
} }
@@ -55,19 +58,20 @@ public class SftpService {
} }
} }
private void configureHostKeyVerification(SSHClient ssh) { private void configureHostKeyVerification(SSHClient ssh) throws SftpException {
if (config.hostKeyFingerprint().isPresent()) { if (config.hostKeyFingerprint().isPresent()) {
ssh.addHostKeyVerifier(config.hostKeyFingerprint().get()); ssh.addHostKeyVerifier(config.hostKeyFingerprint().get());
} else { } else {
Log.warn("SFTP Host-Key-Fingerprint nicht konfiguriert — PromiscuousVerifier aktiv (nur Dev!)"); throw new SftpException("SFTP Host-Key-Fingerprint nicht konfiguriert — Verbindung abgelehnt");
ssh.addHostKeyVerifier(new PromiscuousVerifier());
} }
} }
private void authenticate(SSHClient ssh) throws IOException { private void authenticate(SSHClient ssh) throws IOException {
if (config.privateKeyPath().isPresent()) { if (config.privateKeyPath().isPresent()) {
Log.infof("SFTP-Authentifizierung via Public-Key für Benutzer '%s'", config.username());
ssh.authPublickey(config.username(), config.privateKeyPath().get()); ssh.authPublickey(config.username(), config.privateKeyPath().get());
} else { } else {
Log.infof("SFTP-Authentifizierung via Passwort für Benutzer '%s'", config.username());
ssh.authPassword(config.username(), config.password()); ssh.authPassword(config.username(), config.password());
} }
} }
@@ -79,6 +83,7 @@ public class SftpService {
* @throws SftpException bei Verbindungs- oder Lesefehler * @throws SftpException bei Verbindungs- oder Lesefehler
*/ */
public List<String> listZipFiles() throws SftpException { public List<String> listZipFiles() throws SftpException {
Log.infof("Lese SFTP-Verzeichnis '%s'", config.remotePath());
return withSftp(sftp -> return withSftp(sftp ->
sftp.ls(config.remotePath()).stream() sftp.ls(config.remotePath()).stream()
.filter(RemoteResourceInfo::isRegularFile) .filter(RemoteResourceInfo::isRegularFile)
@@ -97,6 +102,7 @@ public class SftpService {
*/ */
public Path download(String filename) throws SftpException { public Path download(String filename) throws SftpException {
Path localFile = Path.of(config.localWorkDir(), filename); Path localFile = Path.of(config.localWorkDir(), filename);
Log.infof("Starte Download: '%s'", filename);
withSftp(sftp -> { withSftp(sftp -> {
sftp.get(config.remotePath() + "/" + filename, localFile.toString()); sftp.get(config.remotePath() + "/" + filename, localFile.toString());
return null; return null;
@@ -106,13 +112,14 @@ public class SftpService {
/** /**
* Benennt eine Datei auf dem Remote-SFTP-Server um. * Benennt eine Datei auf dem Remote-SFTP-Server um.
* Wird nach Erfolg ({@code .processed}) oder Fehler ({@code .error}) aufgerufen. * Wird nach Fehler ({@code .error}) aufgerufen.
* *
* @param filename aktueller Dateiname, z.B. {@code export_2026-04-08.zip} * @param filename aktueller Dateiname, z.B. {@code export_2026-04-08.zip}
* @param newFilename neuer Dateiname, z.B. {@code export_2026-04-08.zip.processed} * @param newFilename neuer Dateiname, z.B. {@code export_2026-04-08.zip.error}
* @throws SftpException bei Verbindungs- oder Umbenennfehler * @throws SftpException bei Verbindungs- oder Umbenennfehler
*/ */
public void renameRemote(String filename, String newFilename) throws SftpException { public void renameFile(String filename, String newFilename) throws SftpException {
Log.infof("SFTP Rename: '%s' → '%s'", filename, newFilename);
withSftp(sftp -> { withSftp(sftp -> {
sftp.rename( sftp.rename(
config.remotePath() + "/" + filename, config.remotePath() + "/" + filename,
@@ -120,5 +127,22 @@ public class SftpService {
); );
return null; return null;
}); });
Log.infof("SFTP Rename erfolgreich: '%s'", newFilename);
}
/**
* Löscht eine Datei auf dem Remote-SFTP-Server.
* Wird nach erfolgreichem Verarbeiten aufgerufen.
*
* @param filename Dateiname, z.B. {@code export_2026-04-08.zip}
* @throws SftpException bei Verbindungs- oder Löschfehler
*/
public void deleteFile(String filename) throws SftpException {
Log.infof("SFTP Delete: '%s'", filename);
withSftp(sftp -> {
sftp.rm(config.remotePath() + "/" + filename);
return null;
});
Log.infof("SFTP Delete erfolgreich: '%s'", filename);
} }
} }

View File

@@ -1,9 +1,12 @@
package de.galabau.dateieingang.zip; package de.galabau.dateieingang.zip;
import de.galabau.dateieingang.config.OciConfig;
import de.galabau.dateieingang.exception.ZipException; import de.galabau.dateieingang.exception.ZipException;
import de.galabau.dateieingang.model.FileEntry; import de.galabau.dateieingang.model.FileEntry;
import de.galabau.dateieingang.model.ProcessingContext; import de.galabau.dateieingang.model.ProcessingContext;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
@@ -19,6 +22,9 @@ import java.util.List;
@ApplicationScoped @ApplicationScoped
public class ZipExtractionService { public class ZipExtractionService {
@Inject
OciConfig ociConfig;
/** /**
* Entpackt die ZIP-Datei aus {@code context.localZipPath} in ein gleichnamiges Unterverzeichnis. * Entpackt die ZIP-Datei aus {@code context.localZipPath} in ein gleichnamiges Unterverzeichnis.
* Setzt {@code context.localExtractDir} und {@code context.extractedFiles}. * Setzt {@code context.localExtractDir} und {@code context.extractedFiles}.
@@ -35,6 +41,7 @@ public class ZipExtractionService {
try { try {
Files.createDirectories(extractDir); Files.createDirectories(extractDir);
Log.infof("Entpacke ZIP '%s'", context.zipFilename);
try (ZipArchiveInputStream zis = new ZipArchiveInputStream( try (ZipArchiveInputStream zis = new ZipArchiveInputStream(
new BufferedInputStream(Files.newInputStream(context.localZipPath)))) { new BufferedInputStream(Files.newInputStream(context.localZipPath)))) {
@@ -51,14 +58,20 @@ public class ZipExtractionService {
Files.copy(zis, targetFile, StandardCopyOption.REPLACE_EXISTING); Files.copy(zis, targetFile, StandardCopyOption.REPLACE_EXISTING);
boolean isMarker = Path.of(entryName).getFileName() boolean isMarker = Path.of(entryName).getFileName()
.toString().equals("_READY_FOR_DB_PROCESSING_"); .toString().equals(ociConfig.markerFilenameDbProcessing());
entries.add(new FileEntry(entryName, Files.size(targetFile), isMarker)); FileEntry fileEntry = new FileEntry(entryName, Files.size(targetFile), isMarker);
entries.add(fileEntry);
Log.infof("Extrahiert: '%s' (%d Bytes)", entryName, fileEntry.fileSize);
if (fileEntry.isMarker) {
Log.infof("Marker-Datei gefunden: '%s'", entryName);
}
} }
} }
} }
context.extractedFiles = entries; context.extractedFiles = entries;
Log.infof("Extraktion abgeschlossen: %d Datei(en) aus '%s'", entries.size(), context.zipFilename);
} catch (IOException e) { } catch (IOException e) {
throw new ZipException("ZIP '" + context.zipFilename + "' konnte nicht entpackt werden: " throw new ZipException("ZIP '" + context.zipFilename + "' konnte nicht entpackt werden: "

View File

@@ -12,33 +12,46 @@ galabau.sftp.password=${GALABAU_SFTP_PASSWORD:}
# Fingerprint auf host: ssh-keyscan <host> | ssh-keygen -lf - # Fingerprint auf host: ssh-keyscan <host> | ssh-keygen -lf -
galabau.sftp.host-key-fingerprint=${GALABAU_SFTP_HOST_KEY_FINGERPRINT:SHA256:xyz} galabau.sftp.host-key-fingerprint=${GALABAU_SFTP_HOST_KEY_FINGERPRINT:SHA256:xyz}
# Verzeichnis auf dem SFTP-Server, in dem der Lieferant ZIP-Dateien ablegt # Verzeichnis auf dem SFTP-Server, in dem der Lieferant ZIP-Dateien ablegt
galabau.sftp.remote-path=${GALABAU_SFTP_REMOTE_PATH:/bundesagenturfuerarbeit/austausch/test/galaeingang} galabau.sftp.remote-path=${GALABAU_SFTP_REMOTE_PATH:/bundesagenturfuerarbeit/austausch/sck-dev/galaeingang}
# Temporäres lokales Verzeichnis für Download + Entpacken — wird nach jeder ZIP bereinigt # Temporäres lokales Verzeichnis für Download + Entpacken — wird nach jeder ZIP bereinigt
galabau.sftp.local-work-dir=/tmp/sftp-work galabau.sftp.local-work-dir=/tmp/sftp-work
# galabau.sftp.private-key-path=/etc/secrets/sftp-key # galabau.sftp.private-key-path=/etc/secrets/sftp-key
# galabau.sftp.private-key-passphrase=${SFTP_KEY_PASSPHRASE} # galabau.sftp.private-key-passphrase=${SFTP_KEY_PASSPHRASE}
# ===== OCI (Stub — noch nicht aktiv) ===== # ===== OCI Object Storage =====
# galabau.oci.namespace=${OCI_NAMESPACE} # Dateiname des DB-Processing-Markers, der nach dem Upload aller Nutzdateien in OCI abgelegt wird
# galabau.oci.region=${OCI_REGION} galabau.oci.marker-filename-db-processing=${OCI_MARKER_FILENAME_DB_PROCESSING:_READY_FOR_DB_PROCESSING_}
# 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}
# ===== ORDS (Stub — noch nicht aktiv) ===== galabau.oci.namespace=${OCI_NAMESPACE}
# galabau.ords.base-url=${GALABAU_ORDS_BASE_URL:http://ords:8080} galabau.oci.region=${OCI_REGION}
# galabau.ords.process-incoming-path=/ords/.../auto_import/process_incoming galabau.oci.bucket=${OCI_BUCKET}
# galabau.ords.api-key=${GALABAU_ORDS_API_KEY} # Root-Prefix im Bucket, muss mit / enden
# quarkus.rest-client.ords-client.url=${galabau.ords.base-url} galabau.oci.tenant-prefix=${OCI_TENANT_PREFIX:testmandant-42/}
# Gemeinsamer Basis-Prefix für alle BA-Eingangs-Pfade, muss mit / enden
galabau.oci.ba-base-prefix=${OCI_BA_BASE_PREFIX:BA/Eingang/}
# BA-Korrespondenzen: Eingangs-Prefix relativ zu ba-base-prefix, muss mit / enden
galabau.oci.korrespondenzen.incoming-prefix=${OCI_KORRESPONDENZEN_INCOMING_PREFIX:Import/BA-Korrespondenzen/}
# BA-Korrespondenzen: Archiv-Prefix relativ zu ba-base-prefix — Jahr wird zur Laufzeit angehängt
galabau.oci.korrespondenzen.archive-prefix=${OCI_KORRESPONDENZEN_ARCHIVE_PREFIX:BA-Korrespondenzen ZIP-Dateien}
# BA-Aufrechnungen: Eingangs-Prefix relativ zu ba-base-prefix, muss mit / enden
galabau.oci.aufrechnungen.incoming-prefix=${OCI_AUFRECHNUNGEN_INCOMING_PREFIX:Import/Aufrechnungen/}
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 =====
# 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. Das hier ist eine einfache weiterleitung auf die env variable GALABAU_ORDS_BASE_URL (s.o.)
quarkus.rest-client.ords-client.url=${galabau.ords.base-url}
# ===== Observability ===== # ===== Observability =====
%prod.quarkus.otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317} %prod.quarkus.otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317}
%dev.quarkus.observability.lgtm.grafana-port=3000 #%dev.quarkus.observability.lgtm.grafana-port=3000
%dev.quarkus.observability.lgtm.otel-grpc-port=4317 #%dev.quarkus.observability.lgtm.otel-grpc-port=4317
%dev.quarkus.otel.logs.enabled=true quarkus.otel.logs.enabled=true
#%prod.quarkus.otel.logs.enabled=true #%prod.quarkus.otel.logs.enabled=true
%prod.quarkus.log.console.json=true #%prod.quarkus.log.console.json=true

View File

@@ -30,7 +30,8 @@ Details zur DB-Verarbeitung: `database/docs/plan_pck_net_storage.md`
│ im letzten Quarkus-Lauf fehlgeschlagen) │ │ im letzten Quarkus-Lauf fehlgeschlagen) │
│ │ │ │
│ 2. Dateieingang Service aufrufen (fire & forget) │ │ 2. Dateieingang Service aufrufen (fire & forget) │
│ HTTP POST /api/process-incoming (Header: X-Api-Key) │ │ HTTP POST /api/process-incoming-ba-korrespondenz |
| (Header: X-Api-Key) │
└────────────────────────────┬────────────────────────────────────┘ └────────────────────────────┬────────────────────────────────────┘
@@ -42,12 +43,16 @@ Details zur DB-Verarbeitung: `database/docs/plan_pck_net_storage.md`
│ 3c. Alle Dateien in OCI eingang/<zip-name>/ hochladen │ │ 3c. Alle Dateien in OCI eingang/<zip-name>/ hochladen │
│ (Unterordner aus der ZIP werden beibehalten) │ │ (Unterordner aus der ZIP werden beibehalten) │
│ → Fehler stoppt Verarbeitung dieser ZIP │ │ → Fehler stoppt Verarbeitung dieser ZIP │
│ 3d. Marker eingang/<zip-name>/_READY_FOR_DB_PROCESSING_ │ 3d. ZIP auf SFTP löschen
hochladen → bei ungültiger ZIP: .error (manuelle Prüfung nötig)
3e. ZIP auf SFTP umbenennen (.processed oder .error) → bei Infrastrukturfehlern: kein Löschen, Retry
→ erst NACH erfolgreichem Marker-Upload 3e. Marker eingang/<zip-name>/_READY_FOR_DB_PROCESSING_
3f. ORDS-Endpunkt aufrufen (pck_auto_import.p_process_incoming_ba_data) hochladen — ERST NACH dem SFTP-Delete (siehe unten)
│ 3g. Lokale Arbeitsdateien löschen │ │ 3f. Lokale Arbeitsdateien löschen │
│ │
│ Nach allen ZIPs (einmalig): │
│ 3g. ORDS-Endpunkt aufrufen │
│ (pck_auto_import.p_process_incoming_ba_data) │
└────────────────────────────┬────────────────────────────────────┘ └────────────────────────────┬────────────────────────────────────┘
@@ -95,30 +100,85 @@ Daran können die Sachbearbeiter erkennen, dass der Ordner nicht mehr automatisc
## Fehlerfall-Verhalten ## Fehlerfall-Verhalten
**Service: Upload einer Datei schlägt fehl** **Service: ZIP ist beschädigt oder ungültig**
- Verarbeitung dieser ZIP stoppt sofort - SFTP: ZIP → `.error` (manuelle Prüfung nötig)
- Kein Marker wird geschrieben, ZIP auf SFTP wird zu `.error` umbenannt - OCI: kein Upload, kein Marker
- ORDS wird nicht aufgerufen - DB: wird nicht aufgerufen
- Bereits hochgeladene Dateien werden beim nächsten Trigger überschrieben (OCI PUT idempotent)
**Service: ORDS-Aufruf schlägt fehl** **Service: SFTP-Download fehlgeschlagen**
- Marker liegt in `eingang/<zip-name>/`, Dateien sind vollständig hochgeladen - SFTP: ZIP bleibt unverändert, wird beim nächsten Stundenlauf erneut versucht
- Beim nächsten Stundenlauf findet APEX Automation den Marker und verarbeitet - OCI: kein Upload, kein Marker
- DB: wird nicht aufgerufen
**Service: OCI-Upload (Dateien) fehlgeschlagen**
- SFTP: ZIP bleibt unverändert, wird beim nächsten Stundenlauf erneut versucht
- OCI: teilweise hochgeladene Dateien bleiben liegen (kein Marker → DB ignoriert den Ordner); beim Retry werden sie überschrieben (OCI PUT ist idempotent)
- DB: wird nicht aufgerufen
**Service: SFTP-Delete fehlgeschlagen**
- SFTP: ZIP bleibt unverändert, wird beim nächsten Stundenlauf erneut versucht
- OCI: Dateien hochgeladen, noch kein Marker (Marker kommt erst nach dem Delete)
- DB: wird nicht aufgerufen
- beim nächsten Stundenlauf werden die Dateien aber nicht importiert, da APEX Automation ohne Marker nichts findet
- d.h. erst nachdem die ZIP Datei erneut abgearbeitet und komplett in OCI hochgeladen wurde (diesmal mit erfolgreichem Delete auf SFTP & Marker in OCI) werden die Dateien abgearbeitet
**Service: OCI-Marker-Upload fehlgeschlagen**
- SFTP: ZIP ist bereits gelöscht — Quarkus greift sie nie wieder auf
- OCI: Dateien vollständig hochgeladen, Marker fehlt → DB-Verarbeitung wird nicht ausgelöst
- DB wird die Dateien wegen dem fehlendem Marker nie automatisiert abarbeiten, aber man sieht das recht einfach über den OCI Dateibrowser in Apex
- DB: wird nicht aufgerufen
- **Manueller Fix:** Marker-Datei `eingang/<zip-name>/_READY_FOR_DB_PROCESSING_` in OCI von Hand anlegen (leere Datei) — APEX Automation verarbeitet den Batch dann beim nächsten Stundenlauf
**Service: ORDS-Aufruf fehlgeschlagen**
- SFTP: ZIP ist bereits gelöscht — Quarkus greift sie nie wieder auf
- OCI: Dateien + Marker vollständig hochgeladen
- DB: APEX Automation findet den Marker beim nächsten Stundenlauf und verarbeitet ihn (Schritt 1) — kein Doppelimport, da Quarkus die gelöschte ZIP nicht erneut verarbeitet
**DB: Verarbeitung einer einzelnen Datei schlägt fehl** **DB: Verarbeitung einer einzelnen Datei schlägt fehl**
- Rollback — Datei bleibt in `eingang/<zip-name>/` - OCI `eingang/`: Datei bleibt in `eingang/<zip-name>/` (Rollback)
- ERROR in `lg_app_log` mit `log_object_ref = eingang/<zip-name>/datei.csv` - OCI `zielordner/`: keine Änderung
- Nächste Dateien im Batch werden weiterverarbeitet - DB: Rollback, ERROR in `lg_app_log` mit `log_object_ref = eingang/<zip-name>/datei.csv`, nächste Dateien im Batch werden weiterverarbeitet
**DB: Batch-Abschluss (nach dem Datei-Loop)** **DB: Batch-Abschluss (nach dem Datei-Loop)**
- DB-Marker (`_READY_FOR_DB_PROCESSING_`) wird **immer gelöscht** — kein automatischer Retry - Alle Dateien erfolgreich: `eingang/<zip-name>/` ist leer, Marker wird gelöscht
- Liegen noch Dateien im Unterordner: SB-Marker (`_BITTE_PRÜFEN_`) wird angelegt → Sachbearbeiter müssen manuell eingreifen - Noch Dateien übrig: Marker wird gelöscht, SB-Marker (`_BITTE_PRÜFEN_`) wird angelegt → Sachbearbeiter müssen manuell eingreifen
- Alle Dateien erfolgreich: INFO in `lg_app_log`, Unterordner ist leer
**DB: p_move_object schlägt nach erfolgreichem Import fehl** **DB: p_move_object schlägt nach erfolgreichem Import fehl**
- Rollback des Imports → sauberer Ausgangszustand - OCI `eingang/`: Datei bleibt in `eingang/<zip-name>/` (Rollback des gesamten Imports)
- Datei bleibt in `eingang/<zip-name>/` - OCI `zielordner/`: keine Änderung
- DB-Marker wird trotzdem am Ende des Loops gelöscht; falls noch Dateien übrig → SB-Marker - DB: Marker wird am Ende des Loops trotzdem gelöscht; falls noch Dateien übrig → SB-Marker
---
## Design-Entscheidung: Marker wird nach dem SFTP-Delete gesetzt
Der OCI-Marker `_READY_FOR_DB_PROCESSING_` wird bewusst **nach** dem SFTP-Delete
hochgeladen — nicht davor. Das erzeugt eine harte Invariante:
> **Marker in OCI vorhanden ↔ ZIP auf SFTP bereits gelöscht**
### Warum ist das wichtig?
APEX Automation ruft `p_process_incoming_ba_data` in jedem Stundenlauf einmal direkt auf
(Schritt 1, Fallback), und Quarkus ruft dieselbe Funktion via ORDS auf (Schritt 3g, schneller Pfad).
Ohne die Invariante könnte folgender Race entstehen:
1. Quarkus lädt Dateien + Marker hoch, schlägt dann beim SFTP-Delete fehl
2. APEX Schritt 1 findet den Marker → importiert Daten
3. Quarkus wiederholt den Lauf, ruft ORDS auf → zweiter Import derselben Daten
Mit der Invariante ist dieser Fall ausgeschlossen: APEX Schritt 1 findet nur dann einen Marker,
wenn die ZIP auf dem SFTP bereits gelöscht ist. Ist sie das, greift Quarkus sie im Retry
nicht mehr an — `listZipFiles()` gibt nur `.zip`-Dateien zurück.
### Einzig verbleibender manueller Fehlerfall
Schlägt der Marker-Upload fehl (nach erfolgreichem SFTP-Delete), ist der Zustand eindeutig
erkennbar: ZIP auf SFTP gelöscht, Dateien in OCI ohne Marker. Manueller Fix: Marker-Datei
in OCI von Hand anlegen. Dieser Fall erfordert keine DB-seitige Idempotenz, da Quarkus
die gelöschte ZIP nicht erneut verarbeitet und ORDS nicht aufruft.
--- ---