From a31dfc03ffe24b694a5c41be749e49dd76e443b3 Mon Sep 17 00:00:00 2001 From: "Simon C. Kessler" Date: Wed, 8 Apr 2026 16:23:02 +0200 Subject: [PATCH] erste version der DB dateien erstellt --- database/docs/plan_pck_net_storage.md | 28 +- .../ords/net_storage_process_incoming.sql | 32 + database/packages/pck_auto_import.pkb | 207 ++++++ database/packages/pck_auto_import.pkh | 6 + database/packages/pck_log.pkb | 152 +++++ database/packages/pck_log.pkh | 26 + database/packages/pck_net_storage.pkb | 628 ++++++++++++++++++ database/packages/pck_net_storage.pkh | 57 ++ database/tables/lg_app_log.tab | 16 + database/types/t_net_storage_row.typ | 10 + database/types/t_net_storage_tab.typ | 2 + 11 files changed, 1154 insertions(+), 10 deletions(-) create mode 100644 database/ords/net_storage_process_incoming.sql create mode 100644 database/packages/pck_auto_import.pkb create mode 100644 database/packages/pck_auto_import.pkh create mode 100644 database/packages/pck_log.pkb create mode 100644 database/packages/pck_log.pkh create mode 100644 database/packages/pck_net_storage.pkb create mode 100644 database/packages/pck_net_storage.pkh create mode 100644 database/tables/lg_app_log.tab create mode 100644 database/types/t_net_storage_row.typ create mode 100644 database/types/t_net_storage_tab.typ diff --git a/database/docs/plan_pck_net_storage.md b/database/docs/plan_pck_net_storage.md index 74882f2..baaa620 100644 --- a/database/docs/plan_pck_net_storage.md +++ b/database/docs/plan_pck_net_storage.md @@ -1,7 +1,7 @@ # Plan: PCK_NET_STORAGE -**Stand:** 2026-04-07 -**Status:** Planung abgeschlossen — bereit zur Implementierung +**Stand:** 2026-04-08 +**Status:** Implementiert --- @@ -18,14 +18,22 @@ Wird in Oracle APEX für einen Dateimanager genutzt (Browse, Upload, Download, R tables/ lg_app_log.tab +types/ + t_net_storage_row.typ ← Schema-Level Type (siehe Hinweis) + t_net_storage_tab.typ ← Nested Table von t_net_storage_row + packages/ - pck_log.pkh - pck_log.pkb - pck_net_storage.pkh - pck_net_storage.pkb + pck_log.pkh / pck_log.pkb + pck_net_storage.pkh / pck_net_storage.pkb + pck_auto_import.pkh / pck_auto_import.pkb + +ords/ + net_storage_process_incoming.sql ``` -Keine Schema-Level Types. +**Hinweis Schema-Level Types:** `TABLE()` in SQL erfordert in Oracle schema-level Types. +`t_net_storage_row` / `t_net_storage_tab` werden ausschließlich intern für den +`sys_refcursor` von `f_list_objects` benötigt — keine Auswirkung auf die öffentliche API. --- @@ -155,7 +163,7 @@ Bucket nicht erreichbar → -20003 via `f_make_request` ### OCI-Struktur -n8n legt Dateien in einem Unterordner ab, der nach der ZIP-Datei benannt ist. +Der Quarkus Automaton legt Dateien in einem Unterordner ab, der nach der ZIP-Datei benannt ist. Unterordner innerhalb der ZIP werden beibehalten: ``` @@ -166,14 +174,14 @@ eingang//_READY_FOR_DB_PROCESSING_ ← Marker ### Marker-Datei -n8n legt nach erfolgreichem Upload aller Dateien eine leere Marker-Datei ab: +Der Quarkus Automaton legt nach erfolgreichem Upload aller Dateien eine leere Marker-Datei ab: `eingang//_READY_FOR_DB_PROCESSING_` Die DB verarbeitet einen Unterordner **ausschließlich wenn der Marker vorhanden ist.** Der Marker wird erst gelöscht wenn **alle** Dateien des Unterordners erfolgreich verarbeitet wurden — so werden fehlgeschlagene Dateien beim nächsten Lauf erneut versucht. -### Prozedur `p_process_incoming_files` (in separatem Fachpackage, nicht in pck_net_storage) +### Prozedur `p_process_incoming_files` (in `pck_auto_import`, nicht in pck_net_storage) ``` 1. Unterordner von eingang/ auflisten (f_list_objects mit delimiter='/') diff --git a/database/ords/net_storage_process_incoming.sql b/database/ords/net_storage_process_incoming.sql new file mode 100644 index 0000000..6131ef5 --- /dev/null +++ b/database/ords/net_storage_process_incoming.sql @@ -0,0 +1,32 @@ +-- ORDS-Endpunkt: POST /ords/{schema}/net_storage/process_incoming +-- Wird vom Dateieingang Service nach erfolgreichem Upload aufgerufen. +-- Bei Fehler greift die APEX Automation beim nächsten Stundenlauf. +-- +-- Hinweis: Das ORDS-Modul 'net_storage' wird angelegt falls nicht vorhanden. +-- Bei bestehendem Modul schlägt define_module fehl — dann auskommentieren. + +begin + ords.define_module( + p_module_name => 'net_storage' + ,p_base_path => '/net_storage/' + ,p_is_published => true + ); + + ords.define_template( + p_module_name => 'net_storage' + ,p_pattern => 'process_incoming' + ); + + ords.define_handler( + p_module_name => 'net_storage' + ,p_pattern => 'process_incoming' + ,p_method => 'POST' + ,p_source_type => ords.source_type_plsql + ,p_source => q'[begin + pck_auto_import.p_process_incoming_files; +end;]' + ); + + commit; +end; +/ diff --git a/database/packages/pck_auto_import.pkb b/database/packages/pck_auto_import.pkb new file mode 100644 index 0000000..852456e --- /dev/null +++ b/database/packages/pck_auto_import.pkb @@ -0,0 +1,207 @@ +create or replace package body pck_auto_import as + + c_module constant varchar2(20) := 'PCK_AUTO_IMPORT'; + c_eingang_prefix constant varchar2(10) := 'eingang/'; + c_marker constant varchar2(50) := '_READY_FOR_DB_PROCESSING_'; + -- Zielordner nach erfolgreicher Verarbeitung — ggf. in sy_parameter auslagern + c_target_prefix constant varchar2(20) := 'verarbeitet/'; + + procedure p_import_file ( + i_object_key in varchar2 + ,i_content in blob + ) + /*Kopf------------------------------------------------------------------------------------------------ + -- Beschreibung: Importiert eine einzelne Datei aus dem OCI Eingangsordner in die Datenbank. + -- Stub — muss pro Dateityp fachlich implementiert werden. + -- Kein Commit hier — wird von p_process_incoming_files übernommen. + ------------------------------------------------------------------------------------------------------ + -- Parameter: i_object_key Vollständiger OCI-Objektkey der zu verarbeitenden Datei + -- i_content Dateiinhalt als BLOB + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Stub erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + begin + -- TODO: Fachliche Verarbeitung implementieren + -- Beispiel: CSV parsen und in Zieltabellen schreiben + null; + end p_import_file; + + procedure p_process_incoming_files + /*Kopf------------------------------------------------------------------------------------------------ + -- Beschreibung: Verarbeitet alle fertigen Eingangs-Batches aus dem OCI Eingangsordner. + -- Wird von ORDS-Endpunkt und APEX Automation aufgerufen. + -- Pro Datei: Import → Move → Commit (Rollback + Fehlereintrag bei Exception). + -- Marker wird gelöscht wenn keine Dateien mehr im Unterordner verbleiben. + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Prozedur erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + l_folder_cursor sys_refcursor; + l_file_cursor sys_refcursor; + l_check_cursor sys_refcursor; + -- Cursor-Felder für Unterordner + l_folder_name varchar2(1024); + l_dummy_size number; + l_dummy_modified date; + l_dummy_is_folder varchar2(1); + l_dummy_etag varchar2(256); + -- Cursor-Felder für Dateien + l_file_name varchar2(1024); + l_file_size number; + l_file_modified date; + l_file_is_folder varchar2(1); + l_file_etag varchar2(256); + -- Verarbeitungsvariablen + l_marker_key varchar2(1024); + l_target_folder varchar2(1024); + l_zip_name varchar2(512); + l_file_content blob; + l_meta pck_net_storage.t_object_meta; + l_remaining number; + begin + -- Unterordner in eingang/ auflisten (Delimiter '/' liefert nur direkte Kinder) + l_folder_cursor := pck_net_storage.f_list_objects( + i_prefix => c_eingang_prefix + ,i_delimiter => '/' + ); + + loop + fetch l_folder_cursor + into l_folder_name, l_dummy_size, l_dummy_modified, l_dummy_is_folder, l_dummy_etag; + exit when l_folder_cursor%notfound; + + -- Nur Unterordner verarbeiten + if l_dummy_is_folder != 'Y' + then + continue; + end if; + + l_marker_key := l_folder_name || c_marker; + + -- Marker prüfen: -20001 = nicht vorhanden → Upload noch nicht abgeschlossen + begin + l_meta := pck_net_storage.f_get_object_metadata(l_marker_key); + exception + when others + then + if sqlcode = -20001 + then + continue; + end if; + raise; + end; + + -- Zip-Namen aus Ordnerpfad ableiten: eingang// → + l_zip_name := substr( + l_folder_name + ,length(c_eingang_prefix) + 1 + ,length(l_folder_name) - length(c_eingang_prefix) - 1 + ); + l_target_folder := c_target_prefix || l_zip_name || '/'; + + -- Alle Dateien im Unterordner auflisten (kein Delimiter = flach, alle Tiefen) + l_file_cursor := pck_net_storage.f_list_objects( + i_prefix => l_folder_name + ,i_delimiter => '' + ); + + loop + fetch l_file_cursor + into l_file_name, l_file_size, l_file_modified, l_file_is_folder, l_file_etag; + exit when l_file_cursor%notfound; + + -- Marker und Pseudo-Ordner überspringen + if l_file_name = l_marker_key or l_file_is_folder = 'Y' + then + continue; + end if; + + begin + -- 1. Dateiinhalt laden + l_file_content := pck_net_storage.f_download_object(l_file_name); + + -- 2. Fachliche Verarbeitung (noch kein Commit) + p_import_file(l_file_name, l_file_content); + + -- 3. Datei in Zielordner verschieben (noch kein Commit) + -- Rollback von p_move_object macht auch den Import rückgängig + pck_net_storage.p_move_object( + i_object_key => l_file_name + ,i_target_prefix => l_target_folder + ); + + commit; + + pck_log.p_info( + i_module => c_module + ,i_action => 'IMPORT_FILE' + ,i_message => 'Datei erfolgreich verarbeitet' + ,i_object_ref => l_file_name + ); + exception + when others + then + rollback; + pck_log.p_error( + i_module => c_module + ,i_action => 'IMPORT_FILE' + ,i_message => 'Fehler bei Dateiverarbeitung: ' || sqlerrm + ,i_detail => to_clob(dbms_utility.format_error_backtrace) + ,i_object_ref => l_file_name + ); + end; + end loop; + + close l_file_cursor; + + -- Prüfen ob noch nicht verarbeitete Dateien im Unterordner verbleiben + l_remaining := 0; + + l_check_cursor := pck_net_storage.f_list_objects( + i_prefix => l_folder_name + ,i_delimiter => '' + ); + + loop + fetch l_check_cursor + into l_file_name, l_file_size, l_file_modified, l_file_is_folder, l_file_etag; + exit when l_check_cursor%notfound; + + if l_file_name != l_marker_key and l_file_is_folder = 'N' + then + l_remaining := l_remaining + 1; + end if; + end loop; + + close l_check_cursor; + + -- Marker löschen wenn Batch vollständig abgeschlossen + if l_remaining = 0 + then + pck_net_storage.p_delete_object(l_marker_key); + + pck_log.p_info( + i_module => c_module + ,i_action => 'PROCESS_INCOMING' + ,i_message => 'Batch abgeschlossen, Marker gelöscht' + ,i_object_ref => l_folder_name + ); + end if; + end loop; + + close l_folder_cursor; + + exception + when others + then + if l_folder_cursor%isopen then close l_folder_cursor; end if; + if l_file_cursor%isopen then close l_file_cursor; end if; + if l_check_cursor%isopen then close l_check_cursor; end if; + raise; + end p_process_incoming_files; + +end pck_auto_import; +/ diff --git a/database/packages/pck_auto_import.pkh b/database/packages/pck_auto_import.pkh new file mode 100644 index 0000000..b416e4b --- /dev/null +++ b/database/packages/pck_auto_import.pkh @@ -0,0 +1,6 @@ +create or replace package pck_auto_import as + + procedure p_process_incoming_files; + +end pck_auto_import; +/ diff --git a/database/packages/pck_log.pkb b/database/packages/pck_log.pkb new file mode 100644 index 0000000..a6dcd64 --- /dev/null +++ b/database/packages/pck_log.pkb @@ -0,0 +1,152 @@ +create or replace package body pck_log as + + procedure p_write ( + i_level in varchar2 + ,i_module in varchar2 + ,i_action in varchar2 + ,i_message in varchar2 + ,i_detail in clob default null + ,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. + ------------------------------------------------------------------------------------------------------ + -- 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) + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Prozedur erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + pragma autonomous_transaction; + l_user varchar2(100 char); + begin + l_user := substr(upper(coalesce( + sys_context('apex$session', 'app_user') + ,sys_context('userenv', 'os_user') + ,sys_context('userenv', 'session_user'))), 1, 100); + + insert into lg_app_log ( + log_timestamp + ,log_level + ,log_module + ,log_action + ,log_object_ref + ,log_message + ,log_detail + ,log_user + ,log_session_id + ) values ( + systimestamp + ,i_level + ,i_module + ,i_action + ,i_object_ref + ,i_message + ,i_detail + ,l_user + ,to_number(sys_context('userenv', 'sessionid')) + ); + commit; + exception + when others + then + rollback; + raise; + end p_write; + + procedure p_info ( + i_module in varchar2 + ,i_action in varchar2 + ,i_message in varchar2 + ,i_object_ref in varchar2 default null + ) + /*Kopf------------------------------------------------------------------------------------------------ + -- Beschreibung: Schreibt einen Info-Log-Eintrag (Level INFO). + ------------------------------------------------------------------------------------------------------ + -- 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) + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Prozedur erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + begin + p_write( + i_level => 'INFO' + ,i_module => i_module + ,i_action => i_action + ,i_message => i_message + ,i_object_ref => i_object_ref + ); + end p_info; + + procedure p_warn ( + i_module in varchar2 + ,i_action in varchar2 + ,i_message in varchar2 + ,i_object_ref in varchar2 default null + ) + /*Kopf------------------------------------------------------------------------------------------------ + -- Beschreibung: Schreibt einen Warn-Log-Eintrag (Level WARN). + ------------------------------------------------------------------------------------------------------ + -- 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) + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Prozedur erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + begin + p_write( + i_level => 'WARN' + ,i_module => i_module + ,i_action => i_action + ,i_message => i_message + ,i_object_ref => i_object_ref + ); + end p_warn; + + procedure p_error ( + i_module in varchar2 + ,i_action in varchar2 + ,i_message in varchar2 + ,i_detail in clob default null + ,i_object_ref in varchar2 default null + ) + /*Kopf------------------------------------------------------------------------------------------------ + -- Beschreibung: Schreibt einen Fehler-Log-Eintrag (Level ERROR). + ------------------------------------------------------------------------------------------------------ + -- Parameter: i_module Aufgerufenes Modul / Package + -- 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) + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Prozedur erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + begin + p_write( + i_level => 'ERROR' + ,i_module => i_module + ,i_action => i_action + ,i_message => i_message + ,i_detail => i_detail + ,i_object_ref => i_object_ref + ); + end p_error; + +end pck_log; +/ diff --git a/database/packages/pck_log.pkh b/database/packages/pck_log.pkh new file mode 100644 index 0000000..78b7aa0 --- /dev/null +++ b/database/packages/pck_log.pkh @@ -0,0 +1,26 @@ +create or replace package pck_log as + + procedure p_info ( + i_module in varchar2 + ,i_action in varchar2 + ,i_message in varchar2 + ,i_object_ref in varchar2 default null + ); + + procedure p_warn ( + i_module in varchar2 + ,i_action in varchar2 + ,i_message in varchar2 + ,i_object_ref in varchar2 default null + ); + + procedure p_error ( + i_module in varchar2 + ,i_action in varchar2 + ,i_message in varchar2 + ,i_detail in clob default null + ,i_object_ref in varchar2 default null + ); + +end pck_log; +/ diff --git a/database/packages/pck_net_storage.pkb b/database/packages/pck_net_storage.pkb new file mode 100644 index 0000000..cf5781d --- /dev/null +++ b/database/packages/pck_net_storage.pkb @@ -0,0 +1,628 @@ +create or replace package body pck_net_storage as + + -- ==================== Private Helpers ==================== + + function f_build_url ( + i_object_key in varchar2 default null + ,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. + ------------------------------------------------------------------------------------------------------ + -- 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 + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Funktion erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + l_base varchar2(1024); + begin + l_base := 'https://objectstorage.' + || pck_system.f_get_par_wert_by_programmid('NET_STORAGE_REGION') + || '.oraclecloud.com/n/' + || pck_system.f_get_par_wert_by_programmid('NET_STORAGE_NAMESPACE') + || '/b/' + || pck_system.f_get_par_wert_by_programmid('NET_STORAGE_BUCKET'); + + if i_action is not null + then + return l_base || '/actions/' || i_action; + elsif i_object_key is not null + then + -- 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'; + end if; + end f_build_url; + + procedure p_assert_allowed (i_object_key in varchar2) + /*Kopf------------------------------------------------------------------------------------------------ + -- Beschreibung: Prüft den Objektschlüssel auf Path-Traversal-Angriffe und Tenant-Scope. + -- Wirft Application Error -20004 bei Path Traversal, -20005 bei Scope-Verletzung. + ------------------------------------------------------------------------------------------------------ + -- Parameter: i_object_key Zu prüfender Objektschlüssel + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Prozedur erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + l_tenant_prefix varchar2(256); + begin + if instr(i_object_key, '..') > 0 + then + raise_application_error(-20004, 'Path traversal attempt detected'); + end if; + + l_tenant_prefix := pck_system.f_get_par_wert_by_programmid('NET_STORAGE_TENANT_ID'); + + 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 + then + raise_application_error(-20005, 'Access denied: outside tenant scope'); + end if; + end if; + end p_assert_allowed; + + function f_make_request ( + i_method in varchar2 + ,i_url in varchar2 + ,i_body_clob in clob default null + ,i_body_blob in blob default null + ,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 (NET_STORAGE_APEX_CREDENTIAL_ID). + ------------------------------------------------------------------------------------------------------ + -- Parameter: i_method HTTP-Methode (GET, PUT, DELETE, POST, HEAD) + -- 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_content_type Optionaler Content-Type Header + ------------------------------------------------------------------------------------------------------ + -- Rückgabe: Response-Body als CLOB (bei HEAD-Requests leer) + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Funktion erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + l_headers apex_web_service.vc_arr2; + l_values apex_web_service.vc_arr2; + l_response clob; + l_status number; + begin + if i_content_type is not null + then + l_headers(1) := 'Content-Type'; + l_values(1) := i_content_type; + end if; + + l_response := apex_web_service.make_rest_request( + p_url => i_url + ,p_http_method => i_method + ,p_body => coalesce(i_body_clob, empty_clob()) + ,p_body_blob => coalesce(i_body_blob, empty_blob()) + ,p_http_headers => l_headers + ,p_http_values => l_values + ,p_credential_static_id => pck_system.f_get_par_wert_by_programmid('NET_STORAGE_APEX_CREDENTIAL_ID') + ); + + l_status := apex_web_service.g_status_code; + + if l_status = 404 + then + raise_application_error(-20001, 'Object not found'); + elsif l_status in (401, 403) + then + raise_application_error(-20002, 'OCI authentication failed'); + elsif l_status = 409 + then + raise_application_error(-20007, 'Object already exists'); + elsif l_status >= 400 + then + raise_application_error(-20006, + 'OCI API error ' || l_status || ': ' || dbms_lob.substr(l_response, 500, 1)); + end if; + + return l_response; + end f_make_request; + + -- Interne Implementierung ohne Rechteprüfung — wird von f_list_objects und p_delete_folder genutzt + function f_list_objects_internal ( + i_prefix in varchar2 + ,i_delimiter in varchar2 + ,i_start_with in varchar2 + ,i_limit in number + ) return sys_refcursor + /*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 intern genutzt. + ------------------------------------------------------------------------------------------------------ + -- Parameter: i_prefix Präfix / Pfad im Bucket (z.B. eingang/) + -- i_delimiter Trennzeichen für Hierarchie-Simulation (/ für direkte Kinder, leer = rekursiv) + -- i_start_with Optionaler Startpunkt für Paginierung + -- i_limit Maximale Anzahl Ergebnisse (0 = unbegrenzt) + ------------------------------------------------------------------------------------------------------ + -- Rückgabe: Ref Cursor mit Spalten (object_name, object_size, last_modified, is_folder, etag) + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Funktion erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + l_url varchar2(4000); + l_response clob; + l_result t_net_storage_tab := t_net_storage_tab(); + l_next_start varchar2(1024); + l_cursor sys_refcursor; + l_count number := 0; + l_done boolean := false; + l_cur_start varchar2(1024) := i_start_with; + c_page_size constant number := 1000; + begin + while not l_done + loop + l_url := f_build_url() + || '?prefix=' || utl_url.escape(i_prefix, false) + || '&delimiter=' || utl_url.escape(i_delimiter, false) + || '&limit=' || c_page_size; + + if l_cur_start is not null + then + l_url := l_url || '&start=' || utl_url.escape(l_cur_start, false); + end if; + + l_response := f_make_request('GET', l_url); + + -- Dateien aus objects-Array einlesen + for rec in ( + select jt.object_name + ,jt.object_size + ,jt.last_modified + ,jt.etag + from json_table(l_response, '$.objects[*]' + columns ( + object_name varchar2(1024) path '$.name' + ,object_size number path '$.size' + ,last_modified varchar2(50) path '$.timeModified' + ,etag varchar2(256) path '$.etag' + )) jt + ) + loop + l_result.extend; + l_result(l_result.last) := t_net_storage_row( + rec.object_name + ,rec.object_size + ,to_date(substr(rec.last_modified, 1, 19), 'YYYY-MM-DD"T"HH24:MI:SS') + ,'N' + ,rec.etag + ); + l_count := l_count + 1; + + if i_limit > 0 and l_count >= i_limit + then + l_done := true; + exit; + end if; + end loop; + + -- Unterordner aus prefixes-Array einlesen + if not l_done + then + for rec in ( + select jt.prefix_name + from json_table(l_response, '$.prefixes[*]' + columns ( + prefix_name varchar2(1024) path '$' + )) jt + ) + loop + l_result.extend; + l_result(l_result.last) := t_net_storage_row( + rec.prefix_name + ,0 + ,null + ,'Y' + ,null + ); + l_count := l_count + 1; + + if i_limit > 0 and l_count >= i_limit + then + l_done := true; + exit; + end if; + end loop; + end if; + + -- Nächste Seite prüfen + if not l_done + then + l_next_start := json_value(l_response, '$.nextStartWith'); + + if l_next_start is null + then + l_done := true; + else + l_cur_start := l_next_start; + end if; + end if; + end loop; + + open l_cursor for + select t.object_name + ,t.object_size + ,t.last_modified + ,t.is_folder + ,t.etag + from table(l_result) t; + + return l_cursor; + end f_list_objects_internal; + + -- ==================== Öffentliche Funktionen ==================== + + function f_list_objects ( + i_prefix in varchar2 + ,i_delimiter in varchar2 default '/' + ,i_start_with in varchar2 default null + ,i_limit in number default 0 + ) return sys_refcursor + /*Kopf------------------------------------------------------------------------------------------------ + -- Beschreibung: Listet Objekte und Unterordner im Bucket mit Rechteprüfung und Scope-Validierung. + ------------------------------------------------------------------------------------------------------ + -- Parameter: i_prefix Präfix / Pfad im Bucket (z.B. eingang/) + -- i_delimiter Trennzeichen für Hierarchie-Simulation (Standard: /) + -- i_start_with Optionaler Startpunkt für Paginierung + -- i_limit Maximale Anzahl Ergebnisse (0 = unbegrenzt) + ------------------------------------------------------------------------------------------------------ + -- Rückgabe: Ref Cursor mit Spalten (object_name, object_size, last_modified, is_folder, etag) + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Funktion erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + begin + pck_mitarbeiterrecht.p_hat_recht('LESEN_ALLES'); + p_assert_allowed(i_prefix); + return f_list_objects_internal(i_prefix, i_delimiter, i_start_with, i_limit); + end f_list_objects; + + 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. + ------------------------------------------------------------------------------------------------------ + -- Parameter: i_object_key Vollständiger Objektschlüssel im Bucket + ------------------------------------------------------------------------------------------------------ + -- Rückgabe: Dateiinhalt als BLOB + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Funktion erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + l_response blob; + l_status number; + begin + pck_mitarbeiterrecht.p_hat_recht('LESEN_ALLES'); + p_assert_allowed(i_object_key); + + 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('NET_STORAGE_APEX_CREDENTIAL_ID') + ); + + l_status := apex_web_service.g_status_code; + + if l_status = 404 + then + raise_application_error(-20001, 'Object not found: ' || i_object_key); + elsif l_status in (401, 403) + then + raise_application_error(-20002, 'OCI authentication failed'); + elsif l_status >= 400 + then + raise_application_error(-20006, 'OCI API error ' || l_status); + end if; + + return l_response; + end f_download_object; + + procedure p_upload_object ( + i_object_key in varchar2 + ,i_content in blob + ,i_content_type in varchar2 + ) + /*Kopf------------------------------------------------------------------------------------------------ + -- 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 + -- SCK 2026-04-08 Prozedur erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + l_response clob; + begin + pck_mitarbeiterrecht.p_hat_recht('SCHREIBEN_ALLES'); + p_assert_allowed(i_object_key); + + 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 + ); + end p_upload_object; + + procedure p_delete_object (i_object_key in varchar2) + /*Kopf------------------------------------------------------------------------------------------------ + -- Beschreibung: Löscht ein einzelnes Objekt aus dem OCI Bucket (DELETE). + ------------------------------------------------------------------------------------------------------ + -- Parameter: i_object_key Vollständiger Objektschlüssel im Bucket + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Prozedur erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + l_response clob; + begin + pck_mitarbeiterrecht.p_hat_recht('ADMIN'); + p_assert_allowed(i_object_key); + + l_response := f_make_request( + i_method => 'DELETE' + ,i_url => f_build_url(i_object_key) + ); + end p_delete_object; + + procedure p_delete_folder (i_prefix in varchar2) + /*Kopf------------------------------------------------------------------------------------------------ + -- Beschreibung: Löscht rekursiv alle Objekte unterhalb eines Präfixes im OCI Bucket. + -- Pseudo-Ordner (is_folder = Y) werden übersprungen. + ------------------------------------------------------------------------------------------------------ + -- Parameter: i_prefix Ordnerpräfix (z.B. eingang/batch-001/) + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Prozedur erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + l_cursor sys_refcursor; + l_object_name varchar2(1024); + l_object_size number; + l_last_modified date; + l_is_folder varchar2(1); + l_etag varchar2(256); + l_response clob; + begin + pck_mitarbeiterrecht.p_hat_recht('ADMIN'); + p_assert_allowed(i_prefix); + + -- Alle Objekte im Prefix auflisten (kein Delimiter = rekursiv, alle Tiefen) + l_cursor := f_list_objects_internal( + i_prefix => i_prefix + ,i_delimiter => '' + ,i_start_with => null + ,i_limit => 0 + ); + + loop + fetch l_cursor into l_object_name, l_object_size, l_last_modified, l_is_folder, l_etag; + exit when l_cursor%notfound; + + -- Nur echte Objekte löschen, keine Pseudo-Ordner + if l_is_folder = 'N' + then + l_response := f_make_request( + i_method => 'DELETE' + ,i_url => f_build_url(l_object_name) + ); + end if; + end loop; + + close l_cursor; + exception + when others + then + if l_cursor%isopen + then + close l_cursor; + end if; + raise; + end p_delete_folder; + + procedure p_rename_object ( + i_object_key in varchar2 + ,i_new_name in varchar2 + ) + /*Kopf------------------------------------------------------------------------------------------------ + -- 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 + -- i_new_name Neuer Dateiname (ohne Pfad) + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Prozedur erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + l_prefix varchar2(1024); + l_new_key varchar2(1024); + l_body clob; + l_response clob; + begin + pck_mitarbeiterrecht.p_hat_recht('SCHREIBEN_ALLES'); + p_assert_allowed(i_object_key); + + -- Verzeichnispfad aus dem aktuellen Key extrahieren + if instr(i_object_key, '/') > 0 + then + l_prefix := substr(i_object_key, 1, instr(i_object_key, '/', -1)); + else + l_prefix := null; + end if; + + l_new_key := l_prefix || i_new_name; + p_assert_allowed(l_new_key); + + l_body := '{"sourceName":"' || replace(replace(i_object_key, '\', '\\'), '"', '\"') + || '","newName":"' || replace(replace(l_new_key, '\', '\\'), '"', '\"') + || '"}'; + + l_response := f_make_request( + i_method => 'POST' + ,i_url => f_build_url(i_action => 'renameObject') + ,i_body_clob => l_body + ,i_content_type => 'application/json' + ); + end p_rename_object; + + procedure p_move_object ( + i_object_key in varchar2 + ,i_target_prefix in varchar2 + ) + /*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. + ------------------------------------------------------------------------------------------------------ + -- 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 + -- SCK 2026-04-08 Prozedur erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + l_filename varchar2(1024); + l_new_key varchar2(1024); + l_body clob; + l_response clob; + begin + pck_mitarbeiterrecht.p_hat_recht('SCHREIBEN_ALLES'); + p_assert_allowed(i_object_key); + + -- Dateinamen aus dem aktuellen Key extrahieren + if instr(i_object_key, '/') > 0 + then + l_filename := substr(i_object_key, instr(i_object_key, '/', -1) + 1); + else + l_filename := i_object_key; + end if; + + l_new_key := i_target_prefix || l_filename; + p_assert_allowed(l_new_key); + + l_body := '{"sourceName":"' || replace(replace(i_object_key, '\', '\\'), '"', '\"') + || '","newName":"' || replace(replace(l_new_key, '\', '\\'), '"', '\"') + || '"}'; + + l_response := f_make_request( + i_method => 'POST' + ,i_url => f_build_url(i_action => 'renameObject') + ,i_body_clob => l_body + ,i_content_type => 'application/json' + ); + end p_move_object; + + procedure p_create_folder ( + i_prefix in varchar2 + ,i_folder_name in varchar2 + ) + /*Kopf------------------------------------------------------------------------------------------------ + -- Beschreibung: Legt einen neuen Ordner im OCI Bucket an. + -- Ordner werden als leeres Objekt mit trailing Slash simuliert. + ------------------------------------------------------------------------------------------------------ + -- Parameter: i_prefix Übergeordneter Pfad inkl. trailing Slash (z.B. eingang/) + -- i_folder_name Name des neuen Ordners (ohne Slash) + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Prozedur erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + l_folder_key varchar2(1024); + l_response clob; + begin + pck_mitarbeiterrecht.p_hat_recht('SCHREIBEN_ALLES'); + p_assert_allowed(i_prefix); + + -- Ordner als leeres Objekt mit trailing Slash anlegen + l_folder_key := i_prefix || i_folder_name || '/'; + p_assert_allowed(l_folder_key); + + l_response := f_make_request( + i_method => 'PUT' + ,i_url => f_build_url(l_folder_key) + ,i_body_blob => empty_blob() + ,i_content_type => 'application/octet-stream' + ); + end p_create_folder; + + 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. + ------------------------------------------------------------------------------------------------------ + -- 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 + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-08 Funktion erstellt + ------------------------------------------------------------------------------------------------Kopf*/ + is + l_response clob; + l_result t_object_meta; + l_hdr_name varchar2(256); + begin + pck_mitarbeiterrecht.p_hat_recht('LESEN_ALLES'); + p_assert_allowed(i_object_key); + + -- HEAD-Anfrage: leerer Response-Body, Metadaten in Response-Headern + l_response := f_make_request( + i_method => 'HEAD' + ,i_url => f_build_url(i_object_key) + ); + + l_result.object_name := i_object_key; + + for i in 1..apex_web_service.g_headers.count + loop + l_hdr_name := lower(apex_web_service.g_headers(i).name); + + case l_hdr_name + when 'content-length' + then + l_result.object_size := to_number(apex_web_service.g_headers(i).value); + when 'content-type' + then + l_result.content_type := apex_web_service.g_headers(i).value; + when 'last-modified' + then + -- HTTP RFC 7231 Format: "Thu, 01 Jan 2026 00:00:00 GMT" + l_result.last_modified := to_date( + apex_web_service.g_headers(i).value + ,'DY, DD MON YYYY HH24:MI:SS "GMT"' + ,'NLS_DATE_LANGUAGE=AMERICAN' + ); + when 'etag' + then + l_result.etag := apex_web_service.g_headers(i).value; + else + null; + end case; + end loop; + + return l_result; + end f_get_object_metadata; + +end pck_net_storage; +/ diff --git a/database/packages/pck_net_storage.pkh b/database/packages/pck_net_storage.pkh new file mode 100644 index 0000000..7dd7bb4 --- /dev/null +++ b/database/packages/pck_net_storage.pkh @@ -0,0 +1,57 @@ +create or replace package pck_net_storage as + + -- Metadaten eines einzelnen OCI-Objekts (HEAD-Anfrage) + type t_object_meta is record ( + object_name varchar2(1024) + ,object_size number + ,last_modified date + ,content_type varchar2(256) + ,etag varchar2(256) + ); + + function f_list_objects ( + i_prefix in varchar2 + ,i_delimiter in varchar2 default '/' + ,i_start_with in varchar2 default null + ,i_limit in number default 0 + ) return sys_refcursor; + + function f_download_object ( + i_object_key in varchar2 + ) return blob; + + procedure p_upload_object ( + i_object_key in varchar2 + ,i_content in blob + ,i_content_type in varchar2 + ); + + procedure p_delete_object ( + i_object_key in varchar2 + ); + + procedure p_delete_folder ( + i_prefix in varchar2 + ); + + procedure p_rename_object ( + i_object_key in varchar2 + ,i_new_name in varchar2 + ); + + procedure p_move_object ( + i_object_key in varchar2 + ,i_target_prefix in varchar2 + ); + + procedure p_create_folder ( + i_prefix in varchar2 + ,i_folder_name in varchar2 + ); + + function f_get_object_metadata ( + i_object_key in varchar2 + ) return t_object_meta; + +end pck_net_storage; +/ diff --git a/database/tables/lg_app_log.tab b/database/tables/lg_app_log.tab new file mode 100644 index 0000000..0385ca4 --- /dev/null +++ b/database/tables/lg_app_log.tab @@ -0,0 +1,16 @@ +create table lg_app_log ( + log_id number generated by default as identity not null enable + ,log_timestamp timestamp not null + ,log_level varchar2(10 char) not null + ,log_module varchar2(100 char) not null + ,log_action varchar2(100 char) + ,log_object_ref varchar2(512 char) + ,log_message varchar2(4000 char) + ,log_detail clob + ,log_user varchar2(100 char) + ,log_session_id number +); + +alter table lg_app_log + add constraint pk_lg_app_log primary key (lg_app_log_id) + using index enable; diff --git a/database/types/t_net_storage_row.typ b/database/types/t_net_storage_row.typ new file mode 100644 index 0000000..fec5189 --- /dev/null +++ b/database/types/t_net_storage_row.typ @@ -0,0 +1,10 @@ +-- 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_name varchar2(1024) + ,object_size number + ,last_modified date + ,is_folder varchar2(1) + ,etag varchar2(256) +); +/ diff --git a/database/types/t_net_storage_tab.typ b/database/types/t_net_storage_tab.typ new file mode 100644 index 0000000..a8a9c66 --- /dev/null +++ b/database/types/t_net_storage_tab.typ @@ -0,0 +1,2 @@ +create or replace type t_net_storage_tab as table of t_net_storage_row; +/