Dateien und Storage
Kurzantwort: Die Bytes liegen im Dateisystem, die Metadaten in der
Datenbank. Der Pfad ist <basePath>/[<tenant>/]<modellpfad>/<fileId> – der
Dateiname im Pfad ist allein die fileId, der Anzeigename steht nur im
Datensatz. Bei auditierten Modellen bleibt beim Überschreiben die alte Fassung
als Version erhalten (umbenannt), ein Rollback kopiert sie zurück.
Zuständigkeiten
Abschnitt betitelt „Zuständigkeiten“flowchart LR
C[Client] -->|multipart oder base64| API[AbstractRestApi]
API -->|Temp-Datei je Teil| SL[AbstractLayer]
SL -->|Rechteprüfung, fileId| FC[LocalFSFileController]
FC --> FU[FileUtils]
RC[RequestContext] --> FU
FP["FileProperties<br/>basePath"] --> FU
MM["MetaClassInfo<br/>filePath, environment, audited"] --> FU
FU --> FS[(Dateisystem / PVC)]
SL -->|Metadaten| DB[cdms-database]
DB --> RDB[(Datenbank)]
| Aufgabe | Ort |
|---|---|
| Multipart entgegennehmen, Temp-Dateien schreiben | cdms-rest-api |
Entscheiden, ob geschrieben/gelesen werden darf; fileId vergeben | cdms-system-layer |
| Vertrag | FileStorageControllerInterface in cdms-commons |
| Pfad bilden, Bytes bewegen, Versionen halten | cdms-localfs-storage |
| Metadaten persistieren | cdms-database |
Anwendungen ohne Dateien
Abschnitt betitelt „Anwendungen ohne Dateien“Das Speicher-Backend ist optional. Ein System, das keine Datei-Modelle hat,
deklariert storage: NONE – dann nimmt der Generator keine Backend-Abhängigkeit
in die pom.xml auf, und die Anwendung startet ohne eine Implementierung des
Vertrags.
Das ist ein gültiger Zustand, kein halb konfigurierter: Eine Anwendung, die keine
Blobs speichert, soll dafür auch keine Abhängigkeit tragen. Die Auswahl steht in
system.yaml:
| Wert | Wirkung |
|---|---|
LOCALFS | Dateisystem-Backend, siehe unten |
S3, AZURE | eigene Artefakte hinter demselben Vertrag |
NONE | kein Backend, keine Abhängigkeit, kein Bean |
Fehlt das Backend und wird trotzdem eine Datei-Operation erreicht, schlägt sie
fehl – mit file-storage-not-configured. Absicht: Ein stiller Leerlauf würde
einen Upload annehmen und verwerfen. Die beiden Fälle „keine Datei-Modelle” und
„Datei-Modelle ohne deklariertes Backend” sind erst in dem Moment
unterscheidbar, in dem eine Datei-Operation läuft – deshalb wird dort geprüft und
nicht beim Start.
Der Pfad
Abschnitt betitelt „Der Pfad“<basePath>/[<tenant>/]<modellpfad>/<fileId>Beispiel MULTI, Mandant t1, Modell FileReport:
/files/t1/filereport/910e2c19-d5d1-4dae-8229-ea5fcbcc6194Dieselbe Datei in SINGLE:
/files/filereport/910e2c19-d5d1-4dae-8229-ea5fcbcc6194| Bestandteil | Herkunft |
|---|---|
basePath | codamai.cdms.persistence.file.basePath, auf / normalisiert |
tenant | nur bei mandantengebundenen Modellen in MULTI |
| Modellpfad | MetaClassInfo.getFilePath(), generiert aus dem API-Pfad |
fileId | UUID, vom System-Layer vergeben, im Datensatz gespeichert |
Der Anzeigename kommt im Pfad nicht vor. Früher hing er dort mit drin – dann verschob ein Umbenennen die Datei und ließ die alte verwaisen, weshalb Umbenennen unterbunden war. Heute ist der Pfad eine reine Funktion der unveränderlichen Identität, und Umbenennen ändert genau eine Spalte.
Zwei Folgen:
- Es gibt kein Sanitizing mehr – kein vom Benutzer eingegebener String erreicht das Dateisystem.
- Der MIME-Typ wird aus dem Anzeigenamen abgeleitet, weil die gespeicherte Datei keine Endung hat.
Mandantentrennung im Storage
Abschnitt betitelt „Mandantentrennung im Storage“Die Trennung ist positional: eine zweite Prüfung beim Lesen gibt es nicht.
Deshalb wird die Mandantenkennung geprüft, nicht bereinigt – sie muss
[a-zA-Z0-9._-]+ entsprechen und darf kein .. enthalten. Bereinigen würde
a/b und ab auf dasselbe Verzeichnis abbilden.
| Situation | Ergebnis |
|---|---|
| Systemmodell, kein Request-Kontext | aus dem gemeinsamen Wurzelverzeichnis bedient |
| mandantengebunden, kein Kontext | CDMS_FILE_CONTEXT_MISSING |
| mandantengebunden, Kontext ohne Mandant | CDMS_FILE_TENANT_REQUIRED |
| mandantengebunden, unzulässige Kennung | CDMS_FILE_TENANT_INVALID |
Die Einordnung des Modells wird vor dem Request-Kontext geprüft – genau wie in der Datenbank. Ein Systemmodell bleibt ohne Kontext verarbeitbar.
USER-Modelle zählen als mandantengebunden: Der Owner-Filter verengt zusätzlich,
er ersetzt die Mandantengrenze nicht.
Die sechs Operationen
Abschnitt betitelt „Die sechs Operationen“void saveFile(ModelMetaInterface meta, FileInterface pFileMeta, Path path);byte[] getFile(ModelMetaInterface meta, FileInterface pFileMeta);void deleteFile(ModelMetaInterface meta, FileInterface pFileMeta);List<Long> listFileVersions(ModelMetaInterface meta, FileInterface pFileMeta);byte[] getFileVersion(ModelMetaInterface meta, FileInterface pFileMeta, long version);OptionalLong rollbackFile(ModelMetaInterface meta, FileInterface pFileMeta, long version);Speichern
Abschnitt betitelt „Speichern“- Fehlt die Quelldatei, schlägt der Aufruf mit
CDMS_FILE_SOURCE_MISSINGfehl. Ein Speicheraufruf, der nichts speichert, darf keinen Erfolg melden – sonst persistiert der System-Layer Metadaten zu Inhalt, den es nie gab. - Ohne
fileIdwird eine erzeugt (normalerweise hat das der System-Layer schon getan). - Zielpfad berechnen, Verzeichnis mit ausdrücklichem Modus anlegen.
- Bei auditierten Modellen wird der bisherige Inhalt vorher als Version umbenannt.
- Der Inhalt wird unter einem privaten Namen im Zielverzeichnis zusammengeführt und per Rename auf den Zielpfad gehoben – innerhalb eines Dateisystems ist das atomar, ein Leser sieht also nie einen halb geschriebenen Stand.
fileSizeaus der abgelegten Datei,mimeTypeaus dem Anzeigenamen überURLConnection.guessContentTypeFromName(...), sonstapplication/octet-stream.IOException→FileStorageException("file-not-saved", …).
Die ermittelten Metadaten werden in das übergebene Objekt zurückgeschrieben; der System-Layer persistiert sie.
Herunterladen
Abschnitt betitelt „Herunterladen“Die Datei wird vollständig in den Speicher gelesen und als byte[]
zurückgegeben. Das begrenzt die Dateigröße praktisch:
- Der Heap-Bedarf wächst mit der Dateigröße je gleichzeitigem Download.
byte[]endet bei 2 GiB, undfileSizeist einInteger.
Löschen
Abschnitt betitelt „Löschen“Erst werden alle aufbewahrten Versionen entfernt, dann die aktuelle Datei
(deleteIfExists, idempotent). Gelöschte Inhalte dürfen nicht unter einer
älteren Version lesbar bleiben – sonst wäre ein Löschbegehren nicht erfüllt.
Aufgerufen wird das aus AbstractLayer.recursiveDelete(...), für das Objekt
selbst und jedes kaskadierte Kind, jeweils über dessen eigenen System-Layer.
Versionierung
Abschnitt betitelt „Versionierung“Ob eine Datei Historie führt, entscheidet das audited-Flag des Modells –
dasselbe Flag, das die Entity auditiert. Es gibt dafür keine eigene Property.
aktuell: <fileId>aufbewahrt: <fileId>.1785574268973- Der aktuelle Inhalt trägt immer den schlichten Namen ohne Suffix.
- Die Versionskennung wird beim Schreiben vergeben und im Feld
fileVersiondes Datensatzes gespeichert – Datensatzstand und Bytes benennen einander. - Der Wert sind Millisekunden seit Epoch zum Schreibzeitpunkt, bei Kollision
hochgezählt: eindeutig je Datei, größer heißt neuer. Als Zeitstempel ist er
nicht zu interpretieren; maßgeblich ist
listFileVersions. - Versionen werden über die fileId gefunden, nicht über den Anzeigenamen – eine Umbenennung lässt die Historie intakt.
rollbackFile in zwei Schritten: erst wird der aktuelle Inhalt unter einer
neuen Kennung aufbewahrt (ein Rollback ist eine Änderung), dann wird die
gewünschte Version auf den schlichten Namen kopiert. Kopiert, nicht
verschoben – die Version bleibt in der Historie und ist erneut wiederherstellbar.
Für ein nicht auditiertes Modell gilt nichts davon: Überschreiben ist endgültig, Wiederherstellung ist Sache der Infrastruktur-Sicherung.
Upload-Wege
Abschnitt betitelt „Upload-Wege“Multipart: Teile data (JSON) und files. Die Zuordnung läuft über den
Dateinamen; das Feld name im Datei-Objekt muss dem Originalnamen entsprechen.
Doppelte oder fehlende Dateinamen werden abgelehnt.
Base64 im JSON: Jedes Objekt mit einem content-String wird – in beliebiger
Tiefe – als Datei erkannt, dekodiert und in eine Temp-Datei geschrieben. Ein
data:-Präfix wird entfernt.
Im Frontend: Eine
Fileniemals als JSON serialisieren. Das ergibt"[object File]". Richtig ist echtesFormData.
Konsistenz zwischen Blob und Metadaten
Abschnitt betitelt „Konsistenz zwischen Blob und Metadaten“Es gibt keine gemeinsame Transaktion. Reihenfolge: erst der Blob (im
recursivePrepare), dann der Datensatz, Commit am Request-Ende.
| Fehler | Ergebnis |
|---|---|
| Verschieben scheitert | Rollback → weder Datensatz noch Blob. Konsistent. |
| Verschieben klappt, Request rollt später zurück | Blob verwaist |
| Datensatz gelöscht, Blob-Löschung scheitert | Blob verwaist |
Temp-Dateien werden nirgends ausdrücklich gelöscht – sie verschwinden dadurch,
dass saveFile sie verschiebt. Jeder Pfad, der das Verschieben nicht erreicht,
lässt eine Temp-Datei im System-Temp-Verzeichnis zurück.
Atomarität und Dateisysteme
Abschnitt betitelt „Atomarität und Dateisysteme“Files.move ist nur innerhalb eines Dateisystems atomar. Im Container liegen
/tmp (Overlay) und basePath (PVC/NFS) regelmäßig auf verschiedenen Mounts;
ein Move quer über die Mount-Grenze wird zu einem Kopieren, und der Zielpfad ist
sichtbar, während er wächst.
Genau deshalb wird der Inhalt nicht direkt auf den Zielpfad geschrieben: Er wird im Zielverzeichnis unter einem privaten Namen zusammengeführt und von dort umbenannt. Beide Pfade liegen dann im selben Dateisystem, und der letzte Schritt ist ein Rename – atomar, unabhängig davon, wo die Temp-Datei herkam.
Das schützt Leser, nicht Schreiber: Zwei gleichzeitige Schreiber benennen beide auf denselben Zielpfad um, der letzte gewinnt, und niemand bemerkt es. Das ordnet der Datensatz, nicht der Speicher.
Austauschbare Backends
Abschnitt betitelt „Austauschbare Backends“Das Modul ist die LOCALFS-Implementierung eines Vertrags mit vorgesehenen
Geschwistern (S3, AZURE, siehe
Repos und Artefakte) und mit NONE
als gültiger Antwort für Systeme ohne Dateien. Nichts hier ist so geschrieben,
dass es „auch für S3 passt” – Objektspeicher-Semantik ist eine andere
Implementierung hinter derselben Schnittstelle, kein Konfigurationsschalter.