# Plan: PCK_NET_STORAGE **Stand:** 2026-04-08 **Status:** Implementiert --- ## Ziel PL/SQL Package `PCK_NET_STORAGE` kapselt alle Zugriffe auf OCI Object Storage über REST API. Wird in Oracle APEX für einen Dateimanager genutzt (Browse, Upload, Download, Rename, Move, Delete). --- ## Dateien ``` 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_auto_import.pkh / pck_auto_import.pkb ``` **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. --- ## Authentifizierung **APEX Web Credentials** (Typ: Oracle Cloud Infrastructure) Static ID: `OCI_OBJ_STORE_CRED` Wird in APEX Admin UI einmalig angelegt mit: OCI User OCID, Tenancy OCID, Fingerprint, Private Key (PEM). In PL/SQL referenziert via `p_credential_static_id => c_credential_id` in `apex_web_service.make_rest_request`. Grund: DBMS_CLOUD ist auf dieser DB nicht installiert. APEX übernimmt OCI HTTP Signature V1 (SHA-256) automatisch. --- ## Konfigurationsparameter Alle zur Laufzeit via `pck_system.f_get_par_wert_by_programmid`: | Parameter-ID | Inhalt | |---|---| | `NETSTORE_BUCKET` | OCI Object Storage Bucket-Name, z.B. `INKA_NET_STORAGE` | | `NETSTORE_NAMESPACE` | OCI Object Storage Namespace, z.B. `frhqaxi5sgcg` | | `NETSTORE_REGION` | OCI Region unter der der Object Storage erreichbar ist, z.B. `eu-frankfurt-1`. | | `NETSTORE_WALLET_PATH` | Der Pfad zur Wallet-Datei auf dem DB Server, die für die Zertifkatsvalidierung bei Verbindungen zum OCI Object Storage genutzt werden muss.
z.B.: `file:/u01/app/oracle/product/19.0.0.0/dbhome_1/wallets/digicert-global-root-g2-wallet` | | `NETSTORE_TENANT_ID` | Erlaubter Root-Prefix, z.B. `mandant_42/` | | `NETSTORE_CRED_ID` | Credential Static ID für apex_web_service calls, z.B. 'OCI_INKA_NET_STORE' | | `NETSTORE_MARKER_DB` | Name der Marker-Datei im Object Store, der von Automaton abgelegt wird, um zu signalisieren, dass der entsprechende Unterordner komplett hochgeladen wurde. Verhindert die Verarbeitung von unvollständig hochgeladenen Ordnern. z.B.: `_READY_FOR_DB_PROCESSING_` | | `NETSTORE_MARKER_SB` | Name der Marker-Datei im Object Store, der von DB beim Verarbeiten angelegt wird, wenn eine oder mehrere Dateien eines ZIPs nicht automatisiert importiert werden konnten. Das soll signalisieren, dass ein Sachbearbeiter die Dateien in diesem Unterordner manuell prüfen und importieren muss. z.B.: `_BITTE_PRÜFEN_` | | `NETSTORE_BA_PREFIX` | Pfad in Object Storage, wo BA-Daten liegen. Muss mit einem `/` enden, z.B. `BA/Eingang/` | | `NETSTORE_BA_IMPORT` | Name des Unterordners von NETSTORE_BA_PREFIX im Object Storage, wo entpackte Dateien, die noch importiert werden müssen, zwischengespeichert werden. | | `BA_IMPORT_SB_MIT_ID` | Mitarbeiter-ID für Import von BA Daten (z.B. Korrespondenzen). Diese Mitarbeiter-ID bekommt eine Wiedervorlage, für jede Datei, die nicht automatisch importiert werden konnte. | | `AUTOMATON_BASE_URL` | Base-URL des Quarkus Dateieingang Service, z.B. `http://dateieingang:8080` | | `AUTOMATON_API_KEY` | API-Key für den Quarkus Dateieingang Service (Header `X-Api-Key`) | --- ## Öffentliche Schnittstelle (Package Spec) ### Record Type `t_object_meta` | Feld | Typ | |---|---| | `object_name` | `varchar2(1024)` | | `object_size` | `number` | | `last_modified` | `date` | | `content_type` | `varchar2(256)` | | `etag` | `varchar2(256)` | ### Öffentliche Prozeduren und Funktionen Namenskonvention: Prozeduren `p_`-Präfix, Funktionen `f_`-Präfix. Parameter immer `i_`-Präfix. | Name | Typ | Signatur | Rückgabe | Anmerkung | |---|---|---|---|---| | `f_list_objects` | Funktion | `(i_prefix, i_delimiter, i_start_with, i_limit)` | `sys_refcursor` | Cursor-Spalten: `object_name, object_size, last_modified, is_folder(Y/N), etag` | | `f_download_object` | Funktion | `(i_object_key)` | `blob` | | | `p_upload_object` | Prozedur | `(i_object_key, i_content blob, i_content_type)` | — | | | `p_delete_object` | Prozedur | `(i_object_key)` | — | | | `p_delete_folder` | Prozedur | `(i_prefix)` | — | intern: f_list_objects-Loop + p_delete_object | | `p_rename_object` | Prozedur | `(i_object_key, i_new_name)` | — | intern: OCI RenameObject API | | `p_move_object` | Prozedur | `(i_object_key, i_target_prefix)` | — | intern: OCI RenameObject API | | `p_create_folder` | Prozedur | `(i_prefix, i_folder_name)` | — | PUT leeres Objekt mit trailing `/` | | `f_get_object_metadata` | Funktion | `(i_object_key)` | `t_object_meta` | HEAD Request | Rechteprüfungen: Zu beginn von delete-operationen muss folgender Aufruf durchgeführt werden, um die Rechte des nutzers zu prüfen: `pck_mitarbeiterrecht.p_hat_recht('ADMIN');` Bei anderen schreib-Operationen, wie rename oder move muss die Prüfung von folgendem Recht erfolgen: `pck_mitarbeiterrecht.p_hat_recht('SCHREIBEN_ALLES');` Bei lese-Operationen wie `f_get_object_metadata` und `f_download_object` muss folgendes Recht geprüft werden: `pck_mitarbeiterrecht.p_hat_recht('LESEN_ALLES');` Schlägt eine Prüfung fehl, wirft die p_hat_recht Prozedur einen speziellen Application Error mit Fehlermeldung für den Benutzer. --- ## Private Helpers (nur Body) | Name | Typ | Zweck | |---|---|---| | `f_get_cfg` | Funktion | Wrapper für `pck_system.f_get_par_wert_by_programmid` | | `f_build_url` | Funktion | Baut vollständige OCI REST URL (`/n/{ns}/b/{bucket}/o/{key}`) | | `p_assert_allowed` | Prozedur | Tenant-Prefix-Guard + Path-Traversal-Prüfung | | `f_make_request` | Funktion | Wrapper für `apex_web_service.make_rest_request`, wertet HTTP-Status aus, gibt Response-Body zurück | --- ## Pagination (`f_list_objects`) OCI liefert max. 1000 Objekte pro Request. `f_list_objects` ruft OCI intern in einer Schleife auf (`nextStartWith`-Token aus JSON-Response) bis alle Seiten geladen sind. Ergebnis wird in einer lokalen Collection gesammelt, am Ende als `sys_refcursor` geöffnet. `i_limit = 0` → unbegrenzt, `i_limit > 0` → Abbruch wenn Limit erreicht. OCI-Response trennt Unterordner (`prefixes`-Array) von Dateien (`objects`-Array) — beide werden eingesammelt, Ordner mit `is_folder = 'Y'`. --- ## Fehlerbehandlung `f_make_request` wirft bei HTTP-Fehler: | HTTP-Status | Fehlercode | Meldung | |---|---|---| | 404 | -20001 | `Object not found: {key}` | | 401 / 403 | -20002 | `OCI authentication failed` | | 409 | -20007 | `Object already exists: {key}` | | sonstige 4xx/5xx | -20006 | `OCI API error {status}: {response body}` | `p_assert_allowed` wirft: | Fall | Fehlercode | Meldung | |---|---|---| | `..` im Pfad | -20004 | `Path traversal attempt detected` | | Pfad außerhalb Tenant-Prefix | -20005 | `Access denied: outside tenant scope` | Bucket nicht erreichbar → -20003 via `f_make_request` --- ## Sicherheit / Tenant-Isolation `p_assert_allowed` wird am Anfang jeder öffentlichen Funktion/Prozedur aufgerufen: 1. Tenant-Prefix laden via `f_get_cfg('NETSTORE_TENANT_ID')` 2. `..`-Sequenzen im übergebenen Key prüfen → -20004 3. Prüfen ob Key mit Tenant-Prefix beginnt → -20005 --- ## Dateieingang-Verarbeitung ### OCI-Struktur Der Quarkus Automaton legt Dateien in einem Unterordner ab, der nach der ZIP-Datei benannt ist. Unterordner innerhalb der ZIP werden beibehalten: ``` eingang//datei1.csv eingang//unterordner/datei3.csv eingang//_READY_FOR_DB_PROCESSING_ ← Marker ``` ### Marker-Datei 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_run_ba_korrespondenz_dateieingang_automation` (in `pck_auto_import`) — APEX Automation Einstiegspunkt Von der APEX Automation (stündlich) aufgerufen. Orchestriert den Gesamtablauf: ``` 1. p_process_incoming_ba_data aufrufen → verarbeitet Batches die bereits in OCI liegen (Fallback, falls ORDS-Aufruf letzten Lauf fehlschlug) → Fehler eskalieren (APEX Automation markiert Lauf als fehlerhaft) 2. HTTP POST an AUTOMATON_BASE_URL/api/process-incoming (Header: X-Api-Key: AUTOMATON_API_KEY) → 202 Accepted: Pipeline gestartet → 409 Conflict: Service läuft bereits — kein zweiter Lauf, kein Fehler → sonstige Fehler: loggen, nicht eskalieren (nächster Lauf führt Schritt 1 wieder aus) ``` ### Prozedur `p_process_incoming_ba_data` (in `pck_auto_import`) — ORDS-Einstiegspunkt ``` 1. Unterordner von eingang/ auflisten (f_list_objects mit delimiter='/') 2. Für jeden Unterordner: a. Marker eingang//_READY_FOR_DB_PROCESSING_ vorhanden? → nein: überspringen (unvollständiger Upload) b. Alle Dateien in eingang// auflisten (ohne Marker) c. Für jede Datei einzeln: begin Fachliche Verarbeitung (Import) — noch kein Commit log_object_ref := 'eingang//datei.csv' ← ZIP aus Pfad ableitbar pck_net_storage.p_move_object(datei → Zielordner//) commit pck_log.p_info(...) exception when others then rollback pck_log.p_error(i_object_ref => 'eingang//datei.csv', ...) end d. Noch Dateien in eingang// (außer Marker)? → nein: pck_net_storage.p_delete_object(Marker) — Batch abgeschlossen → ja: Marker bleibt — nächster Lauf versucht verbleibende Dateien ``` Rollback nach fehlgeschlagenem `p_move_object` macht auch den Import rückgängig. Kein Verhalten wird aus `lg_app_log` abgeleitet — es dient ausschließlich dem Audit. ### ORDS-Endpunkt `POST /ords/.../net_storage/process_incoming_ba_data` → ruft `p_process_incoming_ba_data` auf. Wird von automaton nach erfolgreichem Upload aufgerufen. Schlägt der Aufruf fehl: APEX Automation greift beim nächsten Stundenlauf. --- ## APEX REST Data Source (manuell einrichten) Für das APEX Interactive Report im Dateimanager-Browser: ``` Name: NET_STORAGE_LIST_OBJECTS Base-URL: https://objectstorage.{region}.oraclecloud.com Endpoint: /n/{namespace}/b/{bucket}/o Method: GET Web Credential: OCI_OBJ_STORE_CRED URL-Parameter: prefix → aktueller Ordner-Prefix (Bind-Variable aus APEX) delimiter → / limit → 1000 start → für Pagination (optional) Spalten-Mapping: objects[*].name → OBJECT_NAME objects[*].size → OBJECT_SIZE objects[*].timeModified → LAST_MODIFIED objects[*].etag → ETAG ``` Hinweis: `prefixes` (Unterordner) und `objects` (Dateien) sind zwei separate JSON-Arrays. Für ein gemeinsames IR entweder zwei REST Sources mit UNION, oder `f_list_objects` über eine APEX Collection als IR-Quelle nutzen. --- ## Logging: lg_app_log + pck_log ### Tabelle `lg_app_log` Allgemeine Anwendungs-Log-Tabelle, modulübergreifend nutzbar. Keine Indizes (Log-Volumen ist gering, Ad-hoc-Suche reicht ohne Index). | Spalte | Typ | Pflicht | Beschreibung | |---|---|---|---| | `lg_app_log_id` | `number generated by default as identity` | ja | PK | | `log_timestamp` | `timestamp` | ja | Zeitpunkt des Log-Eintrags | | `log_level` | `varchar2(10 char)` | ja | `INFO`, `WARN`, `ERROR` | | `log_module` | `varchar2(100 char)` | ja | z.B. `PCK_NET_STORAGE` | | `log_action` | `varchar2(100 char)` | nein | z.B. `PROCESS_FILE` | | `log_object_ref` | `varchar2(512 char)` | nein | z.B. `eingang/datei.csv` | | `log_message` | `varchar2(4000 char)` | nein | Kurzmeldung | | `log_detail` | `clob` | nein | Stack Trace, JSON, etc. | | `log_user` | `varchar2(100 char)` | nein | APEX- oder DB-User | | `log_session_id` | `number` | nein | Oracle Session-ID | Keine Audit-Spalten (Log-Tabelle ist self-auditing durch `log_timestamp` + `log_user`). ### Package `pck_log` Öffentliche Prozeduren: | Name | Signatur | |---|---| | `p_info` | `(i_module, i_action, i_message, i_object_ref default null)` | | `p_warn` | `(i_module, i_action, i_message, i_object_ref default null)` | | `p_error` | `(i_module, i_action, i_message, i_detail default null, i_object_ref default null)` | Intern immer `PRAGMA AUTONOMOUS_TRANSACTION` + `COMMIT` — Log-Einträge sind transaktionsunabhängig und überleben Rollbacks der Haupttransaktion. --- ## OCI API Endpunkte (Referenz) | Operation | Method | Pfad | |---|---|---| | List Objects | GET | `/n/{ns}/b/{bucket}/o?prefix=...&delimiter=/` | | Get Object | GET | `/n/{ns}/b/{bucket}/o/{objectName}` | | Put Object | PUT | `/n/{ns}/b/{bucket}/o/{objectName}` | | Delete Object | DELETE | `/n/{ns}/b/{bucket}/o/{objectName}` | | Head Object | HEAD | `/n/{ns}/b/{bucket}/o/{objectName}` | | Rename Object | POST | `/n/{ns}/b/{bucket}/actions/renameObject` |