create or replace package body pck_net_storage as c_log_module constant lg_app_log.log_module%type := 'NETZLAUFWERK'; -- ==================== 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 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 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: Collection t_net_storage_tab mit allen gefundenen Objekten ------------------------------------------------------------------------------------------------------ -- 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_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; return l_result; 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 t_net_storage_tab /*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: Collection t_net_storage_tab mit allen gefundenen Objekten ------------------------------------------------------------------------------------------------------ -- 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 ); pck_log.p_info( i_module => c_log_module ,i_action => 'UPLOAD' ,i_message => 'Datei hochgeladen' ,i_object_ref => i_object_key ); 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) ); pck_log.p_info( i_module => c_log_module ,i_action => 'DELETE' ,i_message => 'Datei gelöscht' ,i_object_ref => 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_objects t_net_storage_tab; 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_objects := f_list_objects_internal( i_prefix => i_prefix ,i_delimiter => '' ,i_start_with => null ,i_limit => 0 ); -- Nur echte Objekte löschen, keine Pseudo-Ordner for rec in (select object_name, is_folder from table(l_objects)) loop if rec.is_folder = 'N' then l_response := f_make_request( i_method => 'DELETE' ,i_url => f_build_url(rec.object_name) ); end if; end loop; pck_log.p_info( i_module => c_log_module ,i_action => 'DELETE_FOLDER' ,i_message => 'Ordner rekursiv gelöscht' ,i_object_ref => i_prefix ); 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); select json_object( 'sourceName' value i_object_key ,'newName' value l_new_key ) into l_body from dual; 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' ); pck_log.p_info( i_module => c_log_module ,i_action => 'RENAME' ,i_message => 'Datei umbenannt: ' || i_object_key || ' -> ' || l_new_key ,i_object_ref => i_object_key ); 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); select json_object( 'sourceName' value i_object_key ,'newName' value l_new_key ) into l_body from dual; 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' ); pck_log.p_info( i_module => c_log_module ,i_action => 'MOVE' ,i_message => 'Datei verschoben: ' || i_object_key || ' -> ' || l_new_key ,i_object_ref => i_object_key ); 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' ); pck_log.p_info( i_module => c_log_module ,i_action => 'CREATE_FOLDER' ,i_message => 'Ordner angelegt' ,i_object_ref => l_folder_key ); 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; /