Dokumentation aktualisiert auf quarkus und besser strukturiert
This commit is contained in:
287
database/docs/plan_pck_net_storage.md
Normal file
287
database/docs/plan_pck_net_storage.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# Plan: PCK_NET_STORAGE
|
||||
|
||||
**Stand:** 2026-04-07
|
||||
**Status:** Planung abgeschlossen — bereit zur Implementierung
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
packages/
|
||||
pck_log.pkh
|
||||
pck_log.pkb
|
||||
pck_net_storage.pkh
|
||||
pck_net_storage.pkb
|
||||
```
|
||||
|
||||
Keine Schema-Level Types.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
n8n legt Dateien in einem Unterordner ab, der nach der ZIP-Datei benannt ist.
|
||||
Unterordner innerhalb der ZIP werden beibehalten:
|
||||
|
||||
```
|
||||
eingang/<zip-name>/datei1.csv
|
||||
eingang/<zip-name>/unterordner/datei3.csv
|
||||
eingang/<zip-name>/_READY_FOR_DB_PROCESSING_ ← Marker
|
||||
```
|
||||
|
||||
### Marker-Datei
|
||||
|
||||
n8n legt nach erfolgreichem Upload aller Dateien eine leere Marker-Datei ab:
|
||||
`eingang/<zip-name>/_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)
|
||||
|
||||
```
|
||||
1. Unterordner von eingang/ auflisten (f_list_objects mit delimiter='/')
|
||||
2. Für jeden Unterordner:
|
||||
a. Marker eingang/<zip-name>/_READY_FOR_DB_PROCESSING_ vorhanden?
|
||||
→ nein: überspringen (unvollständiger Upload)
|
||||
b. Alle Dateien in eingang/<zip-name>/ auflisten (ohne Marker)
|
||||
c. Für jede Datei einzeln:
|
||||
begin
|
||||
Fachliche Verarbeitung (Import) — noch kein Commit
|
||||
log_object_ref := 'eingang/<zip-name>/datei.csv' ← ZIP aus Pfad ableitbar
|
||||
pck_net_storage.p_move_object(datei → Zielordner/<zip-name>/)
|
||||
commit
|
||||
pck_log.p_info(...)
|
||||
exception when others then
|
||||
rollback
|
||||
pck_log.p_error(i_object_ref => 'eingang/<zip-name>/datei.csv', ...)
|
||||
end
|
||||
d. Noch Dateien in eingang/<zip-name>/ (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` → ruft `p_process_incoming_files` auf.
|
||||
Wird von n8n 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` |
|
||||
Reference in New Issue
Block a user