# 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 | |---|---| | `NET_STORAGE_BUCKET` | Bucket-Name | | `NET_STORAGE_NAMESPACE` | OCI Object Storage Namespace | | `NET_STORAGE_REGION` | z.B. `eu-frankfurt-1` | | `NET_STORAGE_TENANT_ID` | Erlaubter Root-Prefix, z.B. `mandant_42/` | | `NET_STORAGE_APEX_CREDENTIAL_ID` | Credential Static ID für apex_web_service calls, z.B. 'OCI_OBJ_STORE_CRED' | --- ## Ö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('NET_STORAGE_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_process_incoming_ba_data` (in `pck_auto_import`, nicht in pck_net_storage) ``` 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` |