Compare commits
27 Commits
25e854d427
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c74e49e11b | |||
| a099d28113 | |||
| a2471befe2 | |||
| e70a9176ca | |||
| 6418e14be3 | |||
| ca7b26509a | |||
| a74b220ed7 | |||
| 99f492d5eb | |||
| 30170d85ac | |||
| b17ce20ee0 | |||
| 051c10dfad | |||
| ba4b28cf44 | |||
| f40a40e622 | |||
| 485211b169 | |||
| 93191ec65d | |||
| 5098dd38a5 | |||
| 838f6e96e0 | |||
| b159bdd351 | |||
| d36b346f98 | |||
| 1c303f1376 | |||
| aa0ed5d763 | |||
| e7fb09069c | |||
| f7a9113a57 | |||
| 9a445288f8 | |||
| cbcc6922a4 | |||
| 599912ef94 | |||
| 8f7fd949f4 |
@@ -7,7 +7,17 @@
|
||||
"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 '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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_SB` | Name der Marker-Datei im Object Store, der von DB beim Verarbeiten angelegt wird, wenn eine oder mehrere Dateien eines ZIPs nicht automatisiert importiert werden konnten. Das soll signalisieren, dass ein Sachbearbeiter die Dateien in diesem Unterordner manuell prüfen und importieren muss. z.B.: `_BITTE_PRÜFEN_` |
|
||||
| `NETSTORE_BA_PREFIX` | Pfad in Object Storage, wo BA-Daten liegen. Muss mit einem `/` enden, z.B. `BA/Eingang/` |
|
||||
| `NETSTORE_BA_IMPORT` | Name des Unterordners von NETSTORE_BA_PREFIX im Object Storage, wo entpackte Dateien, die noch importiert werden müssen, zwischengespeichert werden. |
|
||||
| `NETSTORE_BA_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. |
|
||||
| `AUTOMATON_BASE_URL` | Base-URL des Quarkus Dateieingang Service, z.B. `http://dateieingang:8080` |
|
||||
| `AUTOMATON_API_KEY` | API-Key für den Quarkus Dateieingang Service (Header `X-Api-Key`) |
|
||||
| `NETSTORE_ORDS_APIKEY` | API-Key, den der Quarkus Server nutzt, um den ORDS aufzurufen |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
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
|
||||
/*Kopf------------------------------------------------------------------------------------------------
|
||||
-- Beschreibung: Einstiegspunkt für die APEX Automation (stündlich).
|
||||
-- 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
|
||||
-- Beschreibung: Einstiegspunkt für die APEX Automation (stündlich).
|
||||
-- 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
|
||||
-- Quarkus-Lauf fehlgeschlagen ist).
|
||||
-- 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.
|
||||
-- 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.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- Parameter: —
|
||||
-- Parameter: —
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- MA Datum Änderung
|
||||
-- MA Datum Änderung
|
||||
-- SCK 2026-04-21 Prozedur erstellt
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
@@ -22,22 +43,48 @@ create or replace package body pck_auto_import as
|
||||
l_response clob;
|
||||
l_http_status number;
|
||||
l_log_action constant varchar2(512 char) := 'BA_KORRESPONDENZEN_DATEIEINGANG_AUTOMATION';
|
||||
l_automaton_endpoint constant varchar2(256 char) := 'api/process-incoming-ba-korrespondenz';
|
||||
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;
|
||||
|
||||
-- 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
|
||||
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');
|
||||
|
||||
apex_web_service.g_request_headers.delete;
|
||||
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;
|
||||
|
||||
l_response := apex_web_service.make_rest_request(
|
||||
p_url => l_service_url
|
||||
,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;
|
||||
|
||||
@@ -47,33 +94,34 @@ create or replace package body pck_auto_import as
|
||||
pck_log.p_info(
|
||||
i_module => c_log_module
|
||||
,i_action => l_log_action
|
||||
,i_message => 'Dateieingang Service angestoßen (202 Accepted)'
|
||||
,i_message => 'BA Dateieingang Service angestoßen (202 Accepted)'
|
||||
);
|
||||
when 409
|
||||
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(
|
||||
i_module => c_log_module
|
||||
,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
|
||||
pck_log.p_warn(
|
||||
pck_log.p_error(
|
||||
i_module => c_log_module
|
||||
,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;
|
||||
exception
|
||||
when others
|
||||
then
|
||||
-- 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(
|
||||
i_module => c_log_module
|
||||
,i_action => l_log_action
|
||||
,i_message => 'Aufruf des Dateieingang Service fehlgeschlagen: ' || sqlerrm
|
||||
,i_detail => to_clob(dbms_utility.format_error_backtrace)
|
||||
,i_message => 'Aufruf des BA Dateieingang Service fehlgeschlagen: ' || sqlerrm
|
||||
,i_detail => to_clob(dbms_utility.format_error_stack) || ' -- Backtrace: -- ' || to_clob(dbms_utility.format_error_backtrace)
|
||||
);
|
||||
end;
|
||||
end p_run_ba_korrespondenz_dateieingang_automation;
|
||||
@@ -86,16 +134,16 @@ create or replace package body pck_auto_import as
|
||||
/*Kopf------------------------------------------------------------------------------------------------
|
||||
-- Beschreibung: Importiert eine einzelne Datei aus dem OCI Eingangsordner in die Datenbank.
|
||||
-- Ruft inkasso.pck_import.f_import_ba_dokument auf.
|
||||
-- Bei Rückgabe 1 (Erfolg): Datei in den Zielordner verschieben.
|
||||
-- Bei Rückgabe != 1 (z.B. ungültiger Dateiname): Warnung loggen und Exception werfen
|
||||
-- — Datei bleibt liegen, Commit/Rollback liegt beim Aufrufer.
|
||||
-- Kein Commit hier — wird von p_process_incoming_ba_data übernommen.
|
||||
-- Bei Rückgabe 1 (Erfolg): Datei in den Zielordner verschieben.
|
||||
-- Bei Rückgabe != 1 (z.B. ungültiger Dateiname): Warnung loggen und Exception werfen
|
||||
-- — Datei bleibt liegen, Commit/Rollback liegt beim Aufrufer.
|
||||
-- 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_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-09 Implementierung: Aufruf f_import_ba_dokument, Wiedervorlage bei Fehler
|
||||
-- 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_file_size := dbms_lob.getlength(i_content);
|
||||
|
||||
l_return := inkasso.pck_import.f_import_ba_dokument(
|
||||
i_datei => i_content
|
||||
,i_dateiname => l_filename
|
||||
,i_datei_groesse => l_file_size
|
||||
pck_log.p_info(
|
||||
i_module => c_log_module
|
||||
,i_action => l_log_action
|
||||
,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
|
||||
then
|
||||
pck_log.p_warn(
|
||||
i_module => c_log_module
|
||||
,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
|
||||
);
|
||||
|
||||
-- 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
|
||||
,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')
|
||||
-- Wiedervorlage für Sachbearbeiter erstellen
|
||||
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 || '").'
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
pck_net_storage.p_move_object(
|
||||
i_object_key => i_object_key
|
||||
@@ -153,13 +236,13 @@ create or replace package body pck_auto_import as
|
||||
/*Kopf------------------------------------------------------------------------------------------------
|
||||
-- Beschreibung: Verarbeitet alle fertigen Eingangs-Batches aus dem OCI Eingangsordner (Netzlaufwerk).
|
||||
-- Wird von ORDS-Endpunkt (von Quarkus Automaton) und aus Apex Automation aufgerufen.
|
||||
-- Pro Datei: Import + Move → Commit; bei Exception: Rollback, Datei bleibt liegen,
|
||||
-- nächste Datei wird trotzdem verarbeitet.
|
||||
-- Nach dem Datei-Loop: DB-Marker immer löschen (verhindert erneuten Durchlauf).
|
||||
-- Pro Datei: Import + Move -> Commit; bei Exception: Rollback, Datei bleibt liegen,
|
||||
-- nächste Datei wird trotzdem verarbeitet.
|
||||
-- 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
|
||||
-- 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-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
|
||||
@@ -178,11 +261,17 @@ create or replace package body pck_auto_import as
|
||||
l_has_remaining_files boolean;
|
||||
l_log_action varchar2(512 char) := 'IMPORT_BA_DATA';
|
||||
begin
|
||||
-- Zielordner Name zusammenstellen
|
||||
l_target_prefix := pck_system.f_get_par_wert_by_programmid('NETSTORE_BA_PREFIX') || 'Verarbeitet ' || to_char(sysdate, 'YYYY') || '/';
|
||||
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');
|
||||
pck_log.p_info(
|
||||
i_module => c_log_module
|
||||
,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(
|
||||
i_parent_folder => l_eingang_prefix
|
||||
,i_include_subfolders => 'N'
|
||||
@@ -196,11 +285,25 @@ create or replace package body pck_auto_import as
|
||||
continue;
|
||||
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.
|
||||
-- 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');
|
||||
|
||||
-- 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
|
||||
l_meta := pck_net_storage.f_get_object_metadata(l_db_processing_marker_key);
|
||||
exception
|
||||
@@ -208,12 +311,18 @@ create or replace package body pck_auto_import as
|
||||
then
|
||||
if sqlcode = -20001
|
||||
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;
|
||||
end if;
|
||||
raise;
|
||||
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(
|
||||
rec_folder.object_key
|
||||
,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 || '/';
|
||||
|
||||
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)
|
||||
l_files := pck_net_storage.f_list_objects(
|
||||
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))
|
||||
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'
|
||||
then
|
||||
continue;
|
||||
end if;
|
||||
|
||||
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
|
||||
l_file_content := pck_net_storage.f_download_object(rec_file.object_key);
|
||||
|
||||
-- 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);
|
||||
|
||||
-- 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
|
||||
-- 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
|
||||
commit;
|
||||
|
||||
exception
|
||||
@@ -260,10 +383,17 @@ create or replace package body pck_auto_import as
|
||||
end;
|
||||
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);
|
||||
|
||||
-- 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(
|
||||
i_parent_folder => rec_folder.object_key
|
||||
,i_include_subfolders => 'Y'
|
||||
@@ -282,28 +412,38 @@ create or replace package body pck_auto_import as
|
||||
|
||||
if l_has_remaining_files
|
||||
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');
|
||||
pck_net_storage.p_upload_object(
|
||||
i_object_key => l_sb_marker_key
|
||||
,i_content => empty_blob()
|
||||
,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
|
||||
pck_log.p_info(
|
||||
i_module => c_log_module
|
||||
,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
|
||||
);
|
||||
end if;
|
||||
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 pck_auto_import;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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;
|
||||
|
||||
-- Von ORDS-Endpunkt aufgerufen (net_storage/process_incoming_ba_data): importiert Dateien aus OCI
|
||||
|
||||
@@ -9,18 +9,18 @@ create or replace package body pck_log as
|
||||
,i_object_ref in varchar2 default null
|
||||
)
|
||||
/*Kopf------------------------------------------------------------------------------------------------
|
||||
-- Beschreibung: Interne Hilfsprozedur — schreibt einen Log-Eintrag in lg_app_log.
|
||||
-- Verwendet autonomous_transaction, damit der Commit unabhängig vom Aufrufer erfolgt.
|
||||
-- Wird ausschließlich von p_info, p_warn und p_error aufgerufen.
|
||||
-- Beschreibung: Interne Hilfsprozedur — schreibt einen Log-Eintrag in lg_app_log.
|
||||
-- Verwendet autonomous_transaction, damit der Commit unabhängig vom Aufrufer erfolgt.
|
||||
-- Wird ausschließlich von p_info, p_warn und p_error aufgerufen.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- Parameter: i_level Log-Level (INFO, WARN, ERROR)
|
||||
-- i_module Aufgerufenes Modul / Package
|
||||
-- i_action Aktion innerhalb des Moduls
|
||||
-- i_message Kurze Meldung
|
||||
-- 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
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
@@ -73,9 +73,9 @@ create or replace package body pck_log as
|
||||
-- Parameter: i_module Aufgerufenes Modul / Package
|
||||
-- i_action Aktion innerhalb des Moduls
|
||||
-- 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
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
@@ -101,9 +101,9 @@ create or replace package body pck_log as
|
||||
-- Parameter: i_module Aufgerufenes Modul / Package
|
||||
-- i_action Aktion innerhalb des Moduls
|
||||
-- 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
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
@@ -131,9 +131,9 @@ create or replace package body pck_log as
|
||||
-- i_action Aktion innerhalb des Moduls
|
||||
-- i_message Kurze Fehlerbeschreibung
|
||||
-- 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
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
|
||||
@@ -9,15 +9,15 @@ create or replace package body pck_net_storage as
|
||||
,i_action in varchar2 default null
|
||||
) return varchar2
|
||||
/*Kopf------------------------------------------------------------------------------------------------
|
||||
-- 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.
|
||||
-- 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.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- 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
|
||||
-- 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
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- 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
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
@@ -35,7 +35,7 @@ create or replace package body pck_net_storage as
|
||||
return l_base || '/actions/' || i_action;
|
||||
elsif i_object_key is not null
|
||||
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);
|
||||
else
|
||||
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)
|
||||
/*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,
|
||||
-- -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-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*/
|
||||
is
|
||||
l_tenant_prefix varchar2(256);
|
||||
l_log_action varchar2(256) := 'ASSERT_ALLOWED';
|
||||
begin
|
||||
if i_object_key is null
|
||||
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');
|
||||
|
||||
/*
|
||||
-- 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
|
||||
then
|
||||
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
|
||||
) return clob
|
||||
/*Kopf------------------------------------------------------------------------------------------------
|
||||
-- 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.
|
||||
-- Authentifizierung erfolgt über APEX Web Credential (NETSTORE_CRED_ID).
|
||||
-- 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.
|
||||
-- Authentifizierung erfolgt über APEX Web Credential (NETSTORE_CRED_ID).
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- 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_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
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- 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-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*/
|
||||
is
|
||||
l_response clob;
|
||||
@@ -121,6 +131,8 @@ create or replace package body pck_net_storage as
|
||||
l_header_index number := 1;
|
||||
l_content_length number;
|
||||
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
|
||||
then
|
||||
@@ -149,10 +161,13 @@ create or replace package body pck_net_storage as
|
||||
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
|
||||
-- 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
|
||||
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).value := l_content_length;
|
||||
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
|
||||
then
|
||||
apex_debug.info('Clob Request Body used:');
|
||||
apex_debug.info(i_body_clob);
|
||||
--apex_debug.info('Clob Request Body used:');
|
||||
--apex_debug.info(i_body_clob);
|
||||
l_response := apex_web_service.make_rest_request(
|
||||
p_url => i_url
|
||||
,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')
|
||||
);
|
||||
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(
|
||||
p_url => i_url
|
||||
,p_http_method => i_method
|
||||
@@ -202,7 +217,7 @@ create or replace package body pck_net_storage as
|
||||
return l_response;
|
||||
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 (
|
||||
i_parent_folder in varchar2
|
||||
,i_include_subfolders in varchar2
|
||||
@@ -210,18 +225,18 @@ create or replace package body pck_net_storage as
|
||||
,i_limit in number
|
||||
) return t_net_storage_tab
|
||||
/*Kopf------------------------------------------------------------------------------------------------
|
||||
-- Beschreibung: Listet Objekte und Unterordner im Bucket ohne Rechte- oder Scope-Prüfung.
|
||||
-- Paginiert automatisch über nextStartWith bis alle Ergebnisse geladen sind.
|
||||
-- Wird von f_list_objects (öffentlich) und p_delete_folder (Leerprüfung) intern genutzt.
|
||||
-- Beschreibung: Listet Objekte und Unterordner im Bucket ohne Rechte- oder Scope-Prüfung.
|
||||
-- Paginiert automatisch über nextStartWith bis alle Ergebnisse geladen sind.
|
||||
-- Wird von f_list_objects (öffentlich) und p_delete_folder (Leerprüfung) intern genutzt.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- Parameter: i_parent_folder Ordnerpfad im Bucket (z.B. eingang/)
|
||||
-- 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)
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- 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
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
@@ -273,7 +288,7 @@ create or replace package body pck_net_storage as
|
||||
,l_obj_path.path
|
||||
,l_obj_path.filename
|
||||
-- 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 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)
|
||||
@@ -320,7 +335,7 @@ create or replace package body pck_net_storage as
|
||||
end loop;
|
||||
end if;
|
||||
|
||||
-- Nächste Seite prüfen
|
||||
-- Nächste Seite prüfen
|
||||
if not l_done
|
||||
then
|
||||
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.
|
||||
-- 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
|
||||
-- und ergänzen fehlende Ordner-Einträge.
|
||||
-- und ergänzen fehlende Ordner-Einträge.
|
||||
declare
|
||||
l_new_folders apex_t_varchar2;
|
||||
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.
|
||||
-- instr(..., '/', 1, level) liefert die Position des n-ten Slashes.
|
||||
-- substr(..., 1, <position>) schneidet den Key bis einschließlich
|
||||
-- dieses Slashes ab — das Ergebnis ist der Ordnerpfad auf Ebene n.
|
||||
-- substr(..., 1, <position>) schneidet den Key bis einschließlich
|
||||
-- dieses Slashes ab — das Ergebnis ist der Ordnerpfad auf Ebene n.
|
||||
--
|
||||
-- Beispiel für 'mandant/Eingang/batch-001/datei.pdf' (3 Slashes):
|
||||
-- level 1 → 'mandant/'
|
||||
-- level 2 → 'mandant/Eingang/'
|
||||
-- level 3 → 'mandant/Eingang/batch-001/'
|
||||
-- Beispiel für 'mandant/Eingang/batch-001/datei.pdf' (3 Slashes):
|
||||
-- level 1 -> 'mandant/'
|
||||
-- level 2 -> 'mandant/Eingang/'
|
||||
-- level 3 -> 'mandant/Eingang/batch-001/'
|
||||
--
|
||||
-- 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,
|
||||
-- da keine echte Eltern-Kind-Beziehung vorliegt.
|
||||
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
|
||||
)
|
||||
-- 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).
|
||||
-- != 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
|
||||
-- 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, '') || '%'
|
||||
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).
|
||||
minus
|
||||
select object_key
|
||||
@@ -402,18 +417,18 @@ create or replace package body pck_net_storage as
|
||||
return l_result;
|
||||
end f_list_objects_internal;
|
||||
|
||||
-- ==================== Öffentliche Funktionen ====================
|
||||
-- ==================== Öffentliche Funktionen ====================
|
||||
|
||||
function f_split_object_key (i_object_key in varchar2) return t_object_path
|
||||
/*Kopf------------------------------------------------------------------------------------------------
|
||||
-- Beschreibung: Extrahiert Pfad und Dateiname aus einem OCI-Objektschlüssel.
|
||||
-- Bei Ordner-Keys (trailing Slash) wird der Ordnername als Dateiname zurückgegeben.
|
||||
-- Beschreibung: Extrahiert Pfad und Dateiname aus einem OCI-Objektschlüssel.
|
||||
-- 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
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
@@ -445,16 +460,16 @@ create or replace package body pck_net_storage as
|
||||
,i_limit in number default 0
|
||||
) return t_net_storage_tab
|
||||
/*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/)
|
||||
-- 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)
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- 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
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
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
|
||||
/*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
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
@@ -486,12 +501,15 @@ create or replace package body pck_net_storage as
|
||||
pck_mitarbeiterrecht.p_hat_recht('LESEN_ALLES');
|
||||
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(
|
||||
p_url => f_build_url(i_object_key)
|
||||
,p_http_method => 'GET'
|
||||
,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;
|
||||
|
||||
if l_status = 404
|
||||
@@ -514,13 +532,13 @@ create or replace package body pck_net_storage as
|
||||
,i_content_type in varchar2
|
||||
)
|
||||
/*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
|
||||
-- i_content Dateiinhalt als BLOB
|
||||
-- i_content_type MIME-Type des Inhalts (z.B. application/octet-stream)
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- MA Datum Änderung
|
||||
-- MA Datum Änderung
|
||||
-- SCK 2026-04-08 Prozedur erstellt
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
@@ -532,16 +550,15 @@ create or replace package body pck_net_storage as
|
||||
|
||||
if substr(i_object_key, -1) = '/'
|
||||
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;
|
||||
|
||||
-- TEST
|
||||
--l_response := f_make_request(
|
||||
-- i_method => 'PUT'
|
||||
-- ,i_url => f_build_url(i_object_key)
|
||||
-- ,i_body_blob => i_content
|
||||
-- ,i_content_type => i_content_type
|
||||
--);
|
||||
l_response := f_make_request(
|
||||
i_method => 'PUT'
|
||||
,i_url => f_build_url(i_object_key)
|
||||
,i_body_blob => i_content
|
||||
,i_content_type => i_content_type
|
||||
);
|
||||
|
||||
l_obj_path := f_split_object_key(i_object_key);
|
||||
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)
|
||||
/*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
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
@@ -577,21 +594,21 @@ create or replace package body pck_net_storage as
|
||||
pck_log.p_info(
|
||||
i_module => c_log_module
|
||||
,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
|
||||
);
|
||||
end p_delete_object;
|
||||
|
||||
procedure p_delete_folder (i_folder_key in varchar2)
|
||||
/*Kopf------------------------------------------------------------------------------------------------
|
||||
-- Beschreibung: Löscht einen leeren Ordner im OCI Bucket.
|
||||
-- Schlägt fehl, wenn noch Objekte oder Unterordner vorhanden sind.
|
||||
-- Beschreibung: Löscht einen leeren Ordner im OCI Bucket.
|
||||
-- Schlägt fehl, wenn noch Objekte oder Unterordner vorhanden sind.
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- 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-10 Rekursives Löschen entfernt — Ordner muss leer sein
|
||||
-- SCK 2026-04-10 Rekursives Löschen entfernt — Ordner muss leer sein
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
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');
|
||||
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(
|
||||
i_parent_folder => l_prefix
|
||||
,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
|
||||
loop
|
||||
apex_debug.info(' [%s] key=%s | is_folder=%s', i, l_objects(i).object_key, l_objects(i).is_folder);
|
||||
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(*)
|
||||
into l_count
|
||||
from table(l_objects)
|
||||
@@ -627,10 +644,10 @@ create or replace package body pck_net_storage as
|
||||
|
||||
if l_count > 0
|
||||
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;
|
||||
|
||||
-- Ordner-Objekt selbst löschen
|
||||
-- Ordner-Objekt selbst löschen
|
||||
l_response := f_make_request(
|
||||
i_method => 'DELETE'
|
||||
,i_url => f_build_url(l_prefix)
|
||||
@@ -640,7 +657,7 @@ create or replace package body pck_net_storage as
|
||||
pck_log.p_info(
|
||||
i_module => c_log_module
|
||||
,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
|
||||
);
|
||||
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.
|
||||
-- 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)
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- MA Datum Änderung
|
||||
-- MA Datum Änderung
|
||||
-- SCK 2026-04-08 Prozedur erstellt
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
@@ -675,7 +692,7 @@ create or replace package body pck_net_storage as
|
||||
|
||||
if instr(i_new_name, '/') > 0
|
||||
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;
|
||||
|
||||
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
|
||||
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;
|
||||
|
||||
select json_object(
|
||||
@@ -716,12 +733,12 @@ create or replace package body pck_net_storage as
|
||||
/*Kopf------------------------------------------------------------------------------------------------
|
||||
-- Beschreibung: Verschiebt ein Objekt in einen anderen Ordner im selben Bucket.
|
||||
-- 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
|
||||
-- i_target_prefix Zielpräfix inkl. trailing Slash (z.B. verarbeitet/batch-001/)
|
||||
-- Parameter: i_object_key Vollständiger Objektschlüssel der Quelldatei
|
||||
-- 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
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
@@ -737,7 +754,7 @@ create or replace package body pck_net_storage as
|
||||
|
||||
if i_target_prefix is null
|
||||
then
|
||||
raise_application_error(-20015, 'Zielpräfix darf nicht null sein');
|
||||
raise_application_error(-20015, 'Zielpräfix darf nicht null sein');
|
||||
end if;
|
||||
|
||||
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.
|
||||
-- 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)
|
||||
------------------------------------------------------------------------------------------------------
|
||||
-- MA Datum Änderung
|
||||
-- MA Datum Änderung
|
||||
-- SCK 2026-04-08 Prozedur erstellt
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
@@ -809,7 +826,7 @@ create or replace package body pck_net_storage as
|
||||
|
||||
if instr(i_folder_name, '/') > 0
|
||||
then
|
||||
raise_application_error(-20011, 'Ordnername darf keinen Schrägstrich enthalten');
|
||||
raise_application_error(-20011, 'Ordnername darf keinen Schrägstrich enthalten');
|
||||
end if;
|
||||
|
||||
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
|
||||
/*Kopf------------------------------------------------------------------------------------------------
|
||||
-- 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
|
||||
------------------------------------------------------------------------------------------------Kopf*/
|
||||
is
|
||||
|
||||
@@ -5,10 +5,10 @@ from lg_app_log
|
||||
--truncate table lg_app_log;
|
||||
|
||||
/*
|
||||
Test Calls für logs
|
||||
Test Calls für logs
|
||||
*/
|
||||
begin
|
||||
-- Neuen Batch-Ordner für heutigen Import anlegen
|
||||
-- Neuen Batch-Ordner für heutigen Import anlegen
|
||||
pck_net_storage.p_create_folder(
|
||||
i_prefix => 'testmandant-42/Eingang/Verarbeitet 2026/'
|
||||
,i_folder_name => 'Batch-2026-04-09'
|
||||
@@ -41,7 +41,7 @@ begin
|
||||
,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(
|
||||
i_object_key => 'testmandant-42/Eingang/Import/scan_003.pdf'
|
||||
);
|
||||
|
||||
34
database/tests/certificate-tests.sql
Normal file
34
database/tests/certificate-tests.sql
Normal 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;
|
||||
/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- 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.
|
||||
-- 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.
|
||||
create or replace type t_net_storage_row as object (
|
||||
object_key varchar2(1024)
|
||||
,object_path varchar2(1024)
|
||||
|
||||
1
quarkus-automaton/.gitignore
vendored
1
quarkus-automaton/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
.env
|
||||
target
|
||||
*private-key.pem
|
||||
3
quarkus-automaton/.vscode/settings.json
vendored
3
quarkus-automaton/.vscode/settings.json
vendored
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
"java.configuration.updateBuildConfiguration": "disabled",
|
||||
"java.compile.nullAnalysis.mode": "disabled"
|
||||
}
|
||||
17
quarkus-automaton/Dockerfile
Normal file
17
quarkus-automaton/Dockerfile
Normal 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"]
|
||||
10
quarkus-automaton/docker/.env-example
Normal file
10
quarkus-automaton/docker/.env-example
Normal 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>
|
||||
19
quarkus-automaton/docker/README.md
Normal file
19
quarkus-automaton/docker/README.md
Normal 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
|
||||
20
quarkus-automaton/docker/build.sh
Normal file
20
quarkus-automaton/docker/build.sh
Normal 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}"
|
||||
@@ -30,16 +30,18 @@ FileProcessingPipeline [ManagedExecutor — Hintergrund-Thread]
|
||||
├─→ OciUploadService.upload() [OCI SDK]
|
||||
│ └─ Dateien in eingang/<zip-name>/ + Marker
|
||||
│
|
||||
├─→ SftpService.renameRemote() [SSHJ]
|
||||
│ └─ .processed (Erfolg) oder .error (Fehler)
|
||||
│
|
||||
├─→ OrdsNotificationService.notify() [MicroProfile REST Client]
|
||||
│ └─ POST pck_auto_import.p_process_incoming_ba_data
|
||||
├─→ SftpService.deleteRemote() [SSHJ]
|
||||
│ └─ ZIP gelöscht (Erfolg) oder .error (Fehler)
|
||||
│
|
||||
└─→ 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
|
||||
|
||||
@@ -110,7 +110,7 @@ quarkus-automaton/
|
||||
| `sftp-download` | `SftpService` | SSHJ | Lädt ZIP in lokales Arbeitsverzeichnis |
|
||||
| `zip-extract` | `ZipExtractionService` | Apache Commons Compress | Entpackt ZIP, preserviert Ordnerstruktur |
|
||||
| `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 |
|
||||
| `cleanup` | `FileProcessingPipeline` | pure Java | Löscht lokale Arbeitsdateien (ZIP + entpackte Dateien) |
|
||||
|
||||
@@ -244,14 +244,14 @@ n8n fire-and-forget-Verhalten.
|
||||
|
||||
### Fehlerklassen
|
||||
|
||||
| Fehler | Typ | Retry | Verhalten |
|
||||
| Fehler | Typ | Umbenennung | Verhalten |
|
||||
|---|---|---|---|
|
||||
| SFTP-Verbindung fehlgeschlagen | transient | nein | Nächster APEX-Lauf (1h) versucht es |
|
||||
| ZIP beschädigt | persistent | nein | ZIP auf SFTP umbenennen zu `.error`, Log |
|
||||
| OCI-Verbindung fehlgeschlagen (z.B. 503) | transient | ja (exponential backoff) | @Retry |
|
||||
| OCI-Upload einer Datei schlägt fehl | persistent | nein | SFTP-Rename zu `.error`, Log — bereits hochgeladene OCI-Dateien bleiben (idempotent) |
|
||||
| ORDS-Aufruf schlägt fehl | transient | ja (2-3x) | Marker liegt vor → APEX Automation schlägt beim nächsten Lauf ein |
|
||||
| Allgemein technischer Fehler | fallabhängig | siehe SmallRye Fault Tolerance | Exception-Log |
|
||||
| SFTP-Verbindung / Download fehlgeschlagen | transient | keine | Datei bleibt auf SFTP — nächster APEX-Lauf (1h) versucht es |
|
||||
| ZIP beschädigt / ungültig | persistent | → `.error` | Datei ist defekt, manuelle Prüfung nötig |
|
||||
| OCI-Verbindung fehlgeschlagen | transient | keine | Datei bleibt auf SFTP — nächster Lauf versucht erneut (OCI PUT 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 | keine (`.processed` bereits gesetzt) | Marker liegt in OCI vor — APEX Automation findet ihn beim nächsten Lauf |
|
||||
| Unerwarteter Laufzeitfehler | fallabhängig | keine | Exception wird geloggt, Datei bleibt auf SFTP |
|
||||
|
||||
### Retry-Strategie (SmallRye Fault Tolerance)
|
||||
|
||||
@@ -309,17 +309,23 @@ Credentials, Fehlerbehandlung).
|
||||
Pipeline.processAll():
|
||||
1. SftpService.listZipFiles() → ["export_2026-04-08.zip", ...]
|
||||
2. für jede ZIP:
|
||||
a. SftpService.download(zip) → lokale Datei
|
||||
b. ZipExtractionService.extract() → ProcessingContext mit FileEntry-Liste
|
||||
c. OciUploadService.upload() → Dateien + Marker in OCI
|
||||
d. SftpService.renameRemote(.processed oder .error)
|
||||
e. OrdsNotificationService.notify()
|
||||
a. SftpService.download(zip) → lokale Datei
|
||||
b. ZipExtractionService.extract() → ProcessingContext mit FileEntry-Liste
|
||||
↳ ZipException → Rename zu .error, Abbruch
|
||||
c. OciUploadService.uploadFiles() → Dateien in OCI (noch kein Marker)
|
||||
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
|
||||
```
|
||||
|
||||
**Cleanup (Schritt f) läuft immer** — in einem `finally`-Block — damit kein Disk-Vollaufen
|
||||
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)
|
||||
@@ -343,11 +349,13 @@ public class OciUploadService {
|
||||
.tenantId(config.tenancyId())
|
||||
.userId(config.userId())
|
||||
.fingerprint(config.fingerprint())
|
||||
.region(Region.fromRegionId(config.region()))
|
||||
.privateKeySupplier(new FilePrivateKeySupplier(config.privateKeyPath()))
|
||||
.privateKeySupplier(() -> Files.newInputStream(Path.of(config.privateKeyPath())))
|
||||
.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>
|
||||
<version>3.44.0</version>
|
||||
</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 -->
|
||||
<dependency>
|
||||
|
||||
@@ -75,11 +75,11 @@
|
||||
<version>0.38.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- OCI Object Storage SDK -->
|
||||
<!-- Aktuelle Version: https://mvnrepository.com/artifact/com.oracle.oci.sdk/oci-java-sdk-objectstorage -->
|
||||
<!-- OCI Object Storage SDK — Shaded Full JAR: Jersey und alle internen Abhängigkeiten sind unter
|
||||
shaded.com.oracle.oci.javasdk.* relokiert, sodass Quarkus RESTEasy die OCI-Provider nicht scannt -->
|
||||
<dependency>
|
||||
<groupId>com.oracle.oci.sdk</groupId>
|
||||
<artifactId>oci-java-sdk-objectstorage</artifactId>
|
||||
<artifactId>oci-java-sdk-shaded-full</artifactId>
|
||||
<version>3.44.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.Map;
|
||||
* REST-Endpunkt für den Dateieingang-Trigger.
|
||||
* Wird von der APEX Automation stündlich per HTTP POST aufgerufen (fire & forget).
|
||||
*/
|
||||
@Path("/api/process-incoming")
|
||||
@Path("/api/process-incoming-ba-korrespondenz")
|
||||
@ApplicationScoped
|
||||
public class FileProcessingResource {
|
||||
|
||||
@@ -38,11 +38,15 @@ public class FileProcessingResource {
|
||||
@POST
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
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)) {
|
||||
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();
|
||||
}
|
||||
|
||||
Log.info("API-Key valide, Pipeline-Trigger wird verarbeitet");
|
||||
boolean started = pipeline.tryProcessAllAsync();
|
||||
|
||||
if (!started) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package de.galabau.dateieingang.sftp;
|
||||
package de.galabau.dateieingang.config;
|
||||
|
||||
import io.smallrye.config.ConfigMapping;
|
||||
|
||||
@@ -12,8 +12,8 @@ import java.util.UUID;
|
||||
*/
|
||||
public class ProcessingContext {
|
||||
|
||||
/** Eindeutige Lauf-ID — wird als MDC-Feld {@code runId} gesetzt. */
|
||||
public final UUID runId;
|
||||
/** Eindeutige Datei-ID — wird als MDC-Feld {@code fileId} gesetzt. */
|
||||
public final UUID fileId;
|
||||
|
||||
/** Originaler ZIP-Dateiname auf dem SFTP-Server, z.B. {@code export_2026-04-08.zip}. */
|
||||
public final String zipFilename;
|
||||
@@ -39,8 +39,8 @@ public class ProcessingContext {
|
||||
/** Aktueller Verarbeitungsstatus. */
|
||||
public ProcessingStatus status = ProcessingStatus.PENDING;
|
||||
|
||||
public ProcessingContext(UUID runId, String zipFilename) {
|
||||
this.runId = runId;
|
||||
public ProcessingContext(UUID fileId, String zipFilename) {
|
||||
this.fileId = fileId;
|
||||
this.zipFilename = zipFilename;
|
||||
this.zipNameWithoutExt = zipFilename.endsWith(".zip")
|
||||
? zipFilename.substring(0, zipFilename.length() - 4)
|
||||
|
||||
@@ -5,6 +5,7 @@ public enum ProcessingStatus {
|
||||
PENDING,
|
||||
PARTIALLY_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,
|
||||
FAILED
|
||||
}
|
||||
|
||||
@@ -1,32 +1,162 @@
|
||||
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.model.FileEntry;
|
||||
import de.galabau.dateieingang.model.ProcessingContext;
|
||||
import de.galabau.dateieingang.model.ProcessingStatus;
|
||||
import io.quarkus.logging.Log;
|
||||
//import io.quarkus.runtime.Startup;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Year;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Lädt die entpackten Dateien und den Marker in OCI Object Storage hoch.
|
||||
*
|
||||
* <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.
|
||||
* Authentifizierung via OCI HTTP Signature V1 (entspricht APEX Web Credential vom Typ OCI).
|
||||
*/
|
||||
//@Startup
|
||||
@ApplicationScoped
|
||||
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
|
||||
* @throws OciException bei persistenten OCI-Fehlern (4xx) nach Retry-Erschöpfung
|
||||
* @throws OciException bei Verbindungs- oder Upload-Fehlern
|
||||
*/
|
||||
public void upload(ProcessingContext context) throws OciException {
|
||||
// TODO: OCI-Upload implementieren (OCI SDK, SimpleAuthenticationDetailsProvider)
|
||||
Log.infof("[STUB] OCI-Upload übersprungen für '%s' (%d Dateien) — wird später implementiert",
|
||||
context.zipNameWithoutExt, context.extractedFiles.size());
|
||||
public void uploadFiles(ProcessingContext context) throws OciException {
|
||||
List<FileEntry> files = context.extractedFiles.stream()
|
||||
.filter(e -> !e.isMarker)
|
||||
.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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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())) + "***";
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,58 @@
|
||||
package de.galabau.dateieingang.ords;
|
||||
|
||||
import de.galabau.dateieingang.config.OrdsConfig;
|
||||
import de.galabau.dateieingang.exception.OrdsException;
|
||||
import de.galabau.dateieingang.model.ProcessingContext;
|
||||
import io.quarkus.logging.Log;
|
||||
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.
|
||||
*
|
||||
* <p><b>Stub:</b> ORDS-Aufruf ist noch nicht implementiert.
|
||||
* Bei einem Ausfall wäre die Verarbeitung ohnehin durch die APEX Automation abgesichert
|
||||
* (diese findet den Marker beim nächsten Stundenlauf).
|
||||
* <p>Bei Ausfall ist die Verarbeitung durch die APEX Automation abgesichert:
|
||||
* Sie findet den Marker beim nächsten Stundenlauf und ruft die Prozedur selbst auf.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class OrdsNotificationService {
|
||||
|
||||
@Inject
|
||||
@RestClient
|
||||
OrdsClient ordsClient;
|
||||
|
||||
@Inject
|
||||
OrdsConfig config;
|
||||
|
||||
/**
|
||||
* Sendet eine Benachrichtigung an den ORDS-Endpunkt.
|
||||
* 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
|
||||
*/
|
||||
public void notify(ProcessingContext context) throws OrdsException {
|
||||
// TODO: ORDS REST-Client implementieren (MicroProfile REST Client + @Retry)
|
||||
Log.infof("[STUB] ORDS-Benachrichtigung übersprungen für '%s' — wird später implementiert",
|
||||
context.zipNameWithoutExt);
|
||||
@Retry(maxRetries = 3, delay = 1000, delayUnit = ChronoUnit.MILLIS,
|
||||
retryOn = OrdsException.class)
|
||||
@Timeout(value = 10, unit = ChronoUnit.SECONDS)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.galabau.dateieingang.pipeline;
|
||||
|
||||
import de.galabau.dateieingang.config.SftpConfig;
|
||||
import de.galabau.dateieingang.exception.OciException;
|
||||
import de.galabau.dateieingang.exception.OrdsException;
|
||||
import de.galabau.dateieingang.exception.SftpException;
|
||||
@@ -19,6 +20,8 @@ import org.slf4j.MDC;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -44,6 +47,9 @@ public class FileProcessingPipeline {
|
||||
@Inject
|
||||
OrdsNotificationService ordsNotificationService;
|
||||
|
||||
@Inject
|
||||
SftpConfig sftpConfig;
|
||||
|
||||
@Inject
|
||||
ManagedExecutor executor;
|
||||
|
||||
@@ -63,6 +69,8 @@ public class FileProcessingPipeline {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
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 {
|
||||
isRunning.set(false);
|
||||
}
|
||||
@@ -70,35 +78,50 @@ public class FileProcessingPipeline {
|
||||
return true;
|
||||
}
|
||||
|
||||
void processAll() {
|
||||
Log.info("Pipeline-Lauf gestartet");
|
||||
private void processAll() {
|
||||
UUID pipelineRunId = UUID.randomUUID();
|
||||
MDC.put("pipelineRunId", pipelineRunId.toString());
|
||||
Log.infof("Pipeline-Lauf gestartet [pipelineRunId=%s]", pipelineRunId);
|
||||
|
||||
List<String> zipFiles;
|
||||
try {
|
||||
zipFiles = sftpService.listZipFiles();
|
||||
} catch (SftpException e) {
|
||||
Log.errorf(e, "SFTP-Listing fehlgeschlagen — Pipeline-Lauf abgebrochen");
|
||||
return;
|
||||
preProcessingCleanup();
|
||||
|
||||
List<String> zipFiles;
|
||||
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) {
|
||||
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 {
|
||||
// --- Download ---
|
||||
@@ -107,66 +130,134 @@ public class FileProcessingPipeline {
|
||||
Log.infof("ZIP '%s' heruntergeladen (%d Bytes)", zipFilename,
|
||||
Files.size(context.localZipPath));
|
||||
|
||||
// --- OCI ZIP-Archiv ---
|
||||
MDC.put("step", "oci-zip-archive");
|
||||
Log.info("Starte ZIP-Upload in OCI");
|
||||
ociUploadService.uploadZipFile(context);
|
||||
|
||||
// --- Entpacken ---
|
||||
MDC.put("step", "zip-extract");
|
||||
zipExtractionService.extract(context);
|
||||
Log.infof("ZIP '%s' entpackt: %d Datei(en)", zipFilename,
|
||||
context.extractedFiles.size());
|
||||
|
||||
// --- OCI Upload (Stub) ---
|
||||
// --- OCI Upload (Dateien, noch kein Marker) ---
|
||||
MDC.put("step", "oci-upload");
|
||||
ociUploadService.upload(context);
|
||||
context.status = ProcessingStatus.PARTIALLY_UPLOADED;
|
||||
Log.info("Starte OCI-Upload");
|
||||
ociUploadService.uploadFiles(context);
|
||||
|
||||
// --- SFTP Rename → .processed ---
|
||||
MDC.put("step", "sftp-rename");
|
||||
sftpService.renameRemote(zipFilename, zipFilename + ".processed");
|
||||
Log.infof("SFTP Rename: '%s' → '%s.processed'", zipFilename, zipFilename);
|
||||
// --- SFTP Delete ---
|
||||
// Erst nach erfolgreichem Datei-Upload — Marker kommt danach,
|
||||
// damit Marker-Präsenz in OCI ↔ ZIP bereits vom SFTP gelöscht ist.
|
||||
MDC.put("step", "sftp-delete");
|
||||
sftpService.deleteFile(zipFilename);
|
||||
|
||||
// --- ORDS Notify (Stub) ---
|
||||
MDC.put("step", "ords-notify");
|
||||
ordsNotificationService.notify(context);
|
||||
// --- OCI Marker ---
|
||||
// Signalisiert der DB-Verarbeitung, dass der Batch vollständig hochgeladen ist.
|
||||
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 (SftpException | ZipException | OciException | OrdsException e) {
|
||||
Log.errorf(e, "Verarbeitung von '%s' fehlgeschlagen: %s", zipFilename, e.getMessage());
|
||||
} catch (ZipException e) {
|
||||
Log.errorf(e, "Ungültige ZIP-Datei '%s' — wird zu .error umbenannt", zipFilename);
|
||||
context.status = ProcessingStatus.FAILED;
|
||||
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) {
|
||||
Log.errorf(e, "I/O-Fehler bei der Verarbeitung von '%s'", zipFilename);
|
||||
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 {
|
||||
cleanup(context);
|
||||
MDC.clear();
|
||||
postProcessingCleanup(context);
|
||||
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) {
|
||||
try {
|
||||
MDC.put("step", "sftp-rename");
|
||||
sftpService.renameRemote(zipFilename, zipFilename + ".error");
|
||||
Log.infof("SFTP Rename: '%s' → '%s.error'", zipFilename, zipFilename);
|
||||
sftpService.renameFile(zipFilename, zipFilename + ".error");
|
||||
} catch (SftpException e) {
|
||||
Log.warnf(e, "Umbenennen zu .error fehlgeschlagen für '%s' — Datei bleibt auf SFTP zur manuellen Prüfung",
|
||||
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 {
|
||||
if (context.localZipPath != null) {
|
||||
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) {
|
||||
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) {
|
||||
Log.warnf(e, "Cleanup für Lauf %s fehlgeschlagen — lokale Dateien verbleiben ggf. in %s",
|
||||
context.runId,
|
||||
Log.warnf(e, "Cleanup für '%s' fehlgeschlagen — lokale Dateien verbleiben ggf. in %s",
|
||||
context.zipFilename,
|
||||
context.localZipPath != null ? context.localZipPath.getParent() : "unbekannt");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.galabau.dateieingang.sftp;
|
||||
|
||||
import de.galabau.dateieingang.config.SftpConfig;
|
||||
import de.galabau.dateieingang.exception.SftpException;
|
||||
import io.quarkus.logging.Log;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
@@ -8,7 +9,6 @@ import jakarta.inject.Inject;
|
||||
import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.sftp.RemoteResourceInfo;
|
||||
import net.schmizz.sshj.sftp.SFTPClient;
|
||||
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -26,6 +26,7 @@ public class SftpService {
|
||||
void init() {
|
||||
try {
|
||||
Files.createDirectories(Path.of(config.localWorkDir()));
|
||||
Log.infof("Lokales Arbeitsverzeichnis: %s", config.localWorkDir());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Lokales Arbeitsverzeichnis konnte nicht erstellt werden: "
|
||||
+ config.localWorkDir(), e);
|
||||
@@ -44,8 +45,10 @@ public class SftpService {
|
||||
private <T> T withSftp(SftpOperation<T> operation) throws SftpException {
|
||||
try (SSHClient ssh = new SSHClient()) {
|
||||
configureHostKeyVerification(ssh);
|
||||
Log.infof("Verbinde zu SFTP %s:%d", config.host(), config.port());
|
||||
ssh.connect(config.host(), config.port());
|
||||
authenticate(ssh);
|
||||
Log.infof("SFTP-Verbindung hergestellt");
|
||||
try (SFTPClient sftp = ssh.newSFTPClient()) {
|
||||
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()) {
|
||||
ssh.addHostKeyVerifier(config.hostKeyFingerprint().get());
|
||||
} else {
|
||||
Log.warn("SFTP Host-Key-Fingerprint nicht konfiguriert — PromiscuousVerifier aktiv (nur Dev!)");
|
||||
ssh.addHostKeyVerifier(new PromiscuousVerifier());
|
||||
throw new SftpException("SFTP Host-Key-Fingerprint nicht konfiguriert — Verbindung abgelehnt");
|
||||
}
|
||||
}
|
||||
|
||||
private void authenticate(SSHClient ssh) throws IOException {
|
||||
if (config.privateKeyPath().isPresent()) {
|
||||
Log.infof("SFTP-Authentifizierung via Public-Key für Benutzer '%s'", config.username());
|
||||
ssh.authPublickey(config.username(), config.privateKeyPath().get());
|
||||
} else {
|
||||
Log.infof("SFTP-Authentifizierung via Passwort für Benutzer '%s'", config.username());
|
||||
ssh.authPassword(config.username(), config.password());
|
||||
}
|
||||
}
|
||||
@@ -79,6 +83,7 @@ public class SftpService {
|
||||
* @throws SftpException bei Verbindungs- oder Lesefehler
|
||||
*/
|
||||
public List<String> listZipFiles() throws SftpException {
|
||||
Log.infof("Lese SFTP-Verzeichnis '%s'", config.remotePath());
|
||||
return withSftp(sftp ->
|
||||
sftp.ls(config.remotePath()).stream()
|
||||
.filter(RemoteResourceInfo::isRegularFile)
|
||||
@@ -97,6 +102,7 @@ public class SftpService {
|
||||
*/
|
||||
public Path download(String filename) throws SftpException {
|
||||
Path localFile = Path.of(config.localWorkDir(), filename);
|
||||
Log.infof("Starte Download: '%s'", filename);
|
||||
withSftp(sftp -> {
|
||||
sftp.get(config.remotePath() + "/" + filename, localFile.toString());
|
||||
return null;
|
||||
@@ -106,13 +112,14 @@ public class SftpService {
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
*/
|
||||
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 -> {
|
||||
sftp.rename(
|
||||
config.remotePath() + "/" + filename,
|
||||
@@ -120,5 +127,22 @@ public class SftpService {
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package de.galabau.dateieingang.zip;
|
||||
|
||||
import de.galabau.dateieingang.config.OciConfig;
|
||||
import de.galabau.dateieingang.exception.ZipException;
|
||||
import de.galabau.dateieingang.model.FileEntry;
|
||||
import de.galabau.dateieingang.model.ProcessingContext;
|
||||
import io.quarkus.logging.Log;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
|
||||
|
||||
@@ -19,6 +22,9 @@ import java.util.List;
|
||||
@ApplicationScoped
|
||||
public class ZipExtractionService {
|
||||
|
||||
@Inject
|
||||
OciConfig ociConfig;
|
||||
|
||||
/**
|
||||
* Entpackt die ZIP-Datei aus {@code context.localZipPath} in ein gleichnamiges Unterverzeichnis.
|
||||
* Setzt {@code context.localExtractDir} und {@code context.extractedFiles}.
|
||||
@@ -35,6 +41,7 @@ public class ZipExtractionService {
|
||||
|
||||
try {
|
||||
Files.createDirectories(extractDir);
|
||||
Log.infof("Entpacke ZIP '%s'", context.zipFilename);
|
||||
|
||||
try (ZipArchiveInputStream zis = new ZipArchiveInputStream(
|
||||
new BufferedInputStream(Files.newInputStream(context.localZipPath)))) {
|
||||
@@ -51,14 +58,20 @@ public class ZipExtractionService {
|
||||
Files.copy(zis, targetFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
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;
|
||||
Log.infof("Extraktion abgeschlossen: %d Datei(en) aus '%s'", entries.size(), context.zipFilename);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new ZipException("ZIP '" + context.zipFilename + "' konnte nicht entpackt werden: "
|
||||
|
||||
@@ -12,33 +12,46 @@ galabau.sftp.password=${GALABAU_SFTP_PASSWORD:}
|
||||
# Fingerprint auf host: ssh-keyscan <host> | ssh-keygen -lf -
|
||||
galabau.sftp.host-key-fingerprint=${GALABAU_SFTP_HOST_KEY_FINGERPRINT:SHA256:xyz}
|
||||
# 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
|
||||
galabau.sftp.local-work-dir=/tmp/sftp-work
|
||||
# galabau.sftp.private-key-path=/etc/secrets/sftp-key
|
||||
# galabau.sftp.private-key-passphrase=${SFTP_KEY_PASSPHRASE}
|
||||
|
||||
# ===== OCI (Stub — noch nicht aktiv) =====
|
||||
# galabau.oci.namespace=${OCI_NAMESPACE}
|
||||
# galabau.oci.region=${OCI_REGION}
|
||||
# galabau.oci.bucket=${OCI_BUCKET}
|
||||
# galabau.oci.tenant-prefix=mandant_42/
|
||||
# galabau.oci.incoming-prefix=eingang/
|
||||
# galabau.oci.tenancy-id=${OCI_TENANCY_ID}
|
||||
# galabau.oci.user-id=${OCI_USER_ID}
|
||||
# galabau.oci.fingerprint=${OCI_FINGERPRINT}
|
||||
# galabau.oci.private-key-path=${OCI_PRIVATE_KEY_PATH}
|
||||
# ===== OCI Object Storage =====
|
||||
# Dateiname des DB-Processing-Markers, der nach dem Upload aller Nutzdateien in OCI abgelegt wird
|
||||
galabau.oci.marker-filename-db-processing=${OCI_MARKER_FILENAME_DB_PROCESSING:_READY_FOR_DB_PROCESSING_}
|
||||
|
||||
# ===== ORDS (Stub — noch nicht aktiv) =====
|
||||
# galabau.ords.base-url=${GALABAU_ORDS_BASE_URL:http://ords:8080}
|
||||
# galabau.ords.process-incoming-path=/ords/.../auto_import/process_incoming
|
||||
# galabau.ords.api-key=${GALABAU_ORDS_API_KEY}
|
||||
# quarkus.rest-client.ords-client.url=${galabau.ords.base-url}
|
||||
galabau.oci.namespace=${OCI_NAMESPACE}
|
||||
galabau.oci.region=${OCI_REGION}
|
||||
galabau.oci.bucket=${OCI_BUCKET}
|
||||
# Root-Prefix im Bucket, muss mit / enden
|
||||
galabau.oci.tenant-prefix=${OCI_TENANT_PREFIX:testmandant-42/}
|
||||
# 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 =====
|
||||
%prod.quarkus.otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317}
|
||||
%dev.quarkus.observability.lgtm.grafana-port=3000
|
||||
%dev.quarkus.observability.lgtm.otel-grpc-port=4317
|
||||
%dev.quarkus.otel.logs.enabled=true
|
||||
#%dev.quarkus.observability.lgtm.grafana-port=3000
|
||||
#%dev.quarkus.observability.lgtm.otel-grpc-port=4317
|
||||
quarkus.otel.logs.enabled=true
|
||||
#%prod.quarkus.otel.logs.enabled=true
|
||||
%prod.quarkus.log.console.json=true
|
||||
#%prod.quarkus.log.console.json=true
|
||||
|
||||
@@ -30,7 +30,8 @@ Details zur DB-Verarbeitung: `database/docs/plan_pck_net_storage.md`
|
||||
│ im letzten Quarkus-Lauf fehlgeschlagen) │
|
||||
│ │
|
||||
│ 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 │
|
||||
│ (Unterordner aus der ZIP werden beibehalten) │
|
||||
│ → Fehler stoppt Verarbeitung dieser ZIP │
|
||||
│ 3d. Marker eingang/<zip-name>/_READY_FOR_DB_PROCESSING_ │
|
||||
│ hochladen │
|
||||
│ 3e. ZIP auf SFTP umbenennen (.processed oder .error) │
|
||||
│ → erst NACH erfolgreichem Marker-Upload │
|
||||
│ 3f. ORDS-Endpunkt aufrufen (pck_auto_import.p_process_incoming_ba_data)│
|
||||
│ 3g. Lokale Arbeitsdateien löschen │
|
||||
│ 3d. ZIP auf SFTP löschen │
|
||||
│ → bei ungültiger ZIP: .error (manuelle Prüfung nötig) │
|
||||
│ → bei Infrastrukturfehlern: kein Löschen, Retry │
|
||||
│ 3e. Marker eingang/<zip-name>/_READY_FOR_DB_PROCESSING_ │
|
||||
│ hochladen — ERST NACH dem SFTP-Delete (siehe unten) │
|
||||
│ 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
|
||||
|
||||
**Service: Upload einer Datei schlägt fehl**
|
||||
- Verarbeitung dieser ZIP stoppt sofort
|
||||
- Kein Marker wird geschrieben, ZIP auf SFTP wird zu `.error` umbenannt
|
||||
- ORDS wird nicht aufgerufen
|
||||
- Bereits hochgeladene Dateien werden beim nächsten Trigger überschrieben (OCI PUT idempotent)
|
||||
**Service: ZIP ist beschädigt oder ungültig**
|
||||
- SFTP: ZIP → `.error` (manuelle Prüfung nötig)
|
||||
- OCI: kein Upload, kein Marker
|
||||
- DB: wird nicht aufgerufen
|
||||
|
||||
**Service: ORDS-Aufruf schlägt fehl**
|
||||
- Marker liegt in `eingang/<zip-name>/`, Dateien sind vollständig hochgeladen
|
||||
- Beim nächsten Stundenlauf findet APEX Automation den Marker und verarbeitet
|
||||
**Service: SFTP-Download fehlgeschlagen**
|
||||
- SFTP: ZIP bleibt unverändert, wird beim nächsten Stundenlauf erneut versucht
|
||||
- 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**
|
||||
- Rollback — Datei bleibt in `eingang/<zip-name>/`
|
||||
- ERROR in `lg_app_log` mit `log_object_ref = eingang/<zip-name>/datei.csv`
|
||||
- Nächste Dateien im Batch werden weiterverarbeitet
|
||||
- OCI `eingang/`: Datei bleibt in `eingang/<zip-name>/` (Rollback)
|
||||
- OCI `zielordner/`: keine Änderung
|
||||
- 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-Marker (`_READY_FOR_DB_PROCESSING_`) wird **immer gelöscht** — kein automatischer Retry
|
||||
- Liegen noch Dateien im Unterordner: SB-Marker (`_BITTE_PRÜFEN_`) wird angelegt → Sachbearbeiter müssen manuell eingreifen
|
||||
- Alle Dateien erfolgreich: INFO in `lg_app_log`, Unterordner ist leer
|
||||
- Alle Dateien erfolgreich: `eingang/<zip-name>/` ist leer, Marker wird gelöscht
|
||||
- Noch Dateien übrig: Marker wird gelöscht, SB-Marker (`_BITTE_PRÜFEN_`) wird angelegt → Sachbearbeiter müssen manuell eingreifen
|
||||
|
||||
**DB: p_move_object schlägt nach erfolgreichem Import fehl**
|
||||
- Rollback des Imports → sauberer Ausgangszustand
|
||||
- Datei bleibt in `eingang/<zip-name>/`
|
||||
- DB-Marker wird trotzdem am Ende des Loops gelöscht; falls noch Dateien übrig → SB-Marker
|
||||
- OCI `eingang/`: Datei bleibt in `eingang/<zip-name>/` (Rollback des gesamten Imports)
|
||||
- OCI `zielordner/`: keine Änderung
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user