erste version der DB dateien erstellt

This commit is contained in:
2026-04-08 16:23:02 +02:00
parent 8a5a44886d
commit a31dfc03ff
11 changed files with 1154 additions and 10 deletions

View File

@@ -1,7 +1,7 @@
# Plan: PCK_NET_STORAGE # Plan: PCK_NET_STORAGE
**Stand:** 2026-04-07 **Stand:** 2026-04-08
**Status:** Planung abgeschlossen — bereit zur Implementierung **Status:** Implementiert
--- ---
@@ -18,14 +18,22 @@ Wird in Oracle APEX für einen Dateimanager genutzt (Browse, Upload, Download, R
tables/ tables/
lg_app_log.tab 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/ packages/
pck_log.pkh pck_log.pkh / pck_log.pkb
pck_log.pkb pck_net_storage.pkh / pck_net_storage.pkb
pck_net_storage.pkh pck_auto_import.pkh / pck_auto_import.pkb
pck_net_storage.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 ### 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: Unterordner innerhalb der ZIP werden beibehalten:
``` ```
@@ -166,14 +174,14 @@ eingang/<zip-name>/_READY_FOR_DB_PROCESSING_ ← Marker
### Marker-Datei ### 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/<zip-name>/_READY_FOR_DB_PROCESSING_` `eingang/<zip-name>/_READY_FOR_DB_PROCESSING_`
Die DB verarbeitet einen Unterordner **ausschließlich wenn der Marker vorhanden ist.** Die DB verarbeitet einen Unterordner **ausschließlich wenn der Marker vorhanden ist.**
Der Marker wird erst gelöscht wenn **alle** Dateien des Unterordners erfolgreich Der Marker wird erst gelöscht wenn **alle** Dateien des Unterordners erfolgreich
verarbeitet wurden — so werden fehlgeschlagene Dateien beim nächsten Lauf erneut versucht. 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='/') 1. Unterordner von eingang/ auflisten (f_list_objects mit delimiter='/')

View File

@@ -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;
/

View File

@@ -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/<zip-name>/ → <zip-name>
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;
/

View File

@@ -0,0 +1,6 @@
create or replace package pck_auto_import as
procedure p_process_incoming_files;
end pck_auto_import;
/

View File

@@ -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;
/

View File

@@ -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;
/

View File

@@ -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;
/

View File

@@ -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;
/

View File

@@ -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;

View File

@@ -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)
);
/

View File

@@ -0,0 +1,2 @@
create or replace type t_net_storage_tab as table of t_net_storage_row;
/