refactor(quick-260922-hk4): Bilderrahmen-Bilder in user-files statt in der Datenbank
- Bytes liegen unter user-files/dashboard-images/<userId>/<id>.<ext>, die Zeile haelt nur noch storagePath (Muster User.avatarPath) - Dateiname immer servergeneriert: UUID der Zeile + Endung aus dem ERKANNTEN Mime-Typ, originalName kommt in keinem Pfad vor (T-HK4-01) - Migration 20260922120000: storagePath dazu, data wird NULLbar, kein DROP (zweistufig, T-HK4-03); system_read_policy fuer den Umzug - onApplicationBootstrap zieht Altbestand automatisch um: systemgebunden lesen, je Zeile mandantengebunden schreiben (Muster DKV-Planer) - Upload nimmt die Zeile bei fehlgeschlagenem Schreiben zurueck, Loeschen entfernt die Datei mit, fehlende Datei -> 404 (T-HK4-04) - 11 neue Dienst-Tests gegen ein echtes Temp-Verzeichnis (kein fs-Mock) - Zugriffsklassifikation: Stand system-gebunden, Zahlen nachgemessen Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
-- quick-260922-hk4 — Bilderrahmen-Bilder wandern aus der Datenbank in den
|
||||
-- Dateibereich (Volume `user-files`).
|
||||
--
|
||||
-- Warum: gesichert wird von Hand per `pg_dump` (docs/anleitung-betrieb.md
|
||||
-- Kap. 6). Jedes Bild waechst in diesen Abzug hinein — 30 Bilder à 5 MiB je
|
||||
-- Benutzer sind im Extremfall 150 MB PRO BENUTZER, gegen eine heute 18 MB
|
||||
-- grosse Datenbank (gemessen 22.09.2026 auf alpha). Die Bytes liegen ab
|
||||
-- dieser Version unter
|
||||
-- `user-files/dashboard-images/<userId>/<id>.<png|jpg|gif|webp>`; die Zeile
|
||||
-- haelt nur noch den relativen Pfad in "storagePath" — dasselbe Muster wie
|
||||
-- `User.avatarPath` (user.controller.ts) und die DKV-Ausfuhren
|
||||
-- (dkv-export.service.ts). Der Dateiname ist IMMER servergeneriert (die
|
||||
-- UUID der Zeile plus die Endung aus dem an den Magic Bytes ERKANNTEN
|
||||
-- Mime-Typ); kein Byte aus der Anfrage, insbesondere nicht
|
||||
-- "originalName", geht je in einen Pfad (T-HK4-01, Muster T-07-09).
|
||||
--
|
||||
-- ZWEISTUFIG, UND WARUM DIESE MIGRATION "data" NICHT LOESCHT (T-HK4-03):
|
||||
-- Vorhandene Zeilen tragen ihre Bytes noch in "data". Der Umzug auf die
|
||||
-- Platte passiert beim ersten Start dieser Version automatisch
|
||||
-- (DashboardImagesService.onApplicationBootstrap, liest systemgebunden ueber
|
||||
-- alle Mandanten, schreibt je Zeile mandantengebunden zurueck) — der Nutzer
|
||||
-- muss nichts ausfuehren. Wuerde diese Migration die Spalte sofort
|
||||
-- loeschen, laufen Migration und Umzug im selben Start in der falschen
|
||||
-- Reihenfolge ("migrate deploy" laeuft VOR dem Anwendungsstart) und die
|
||||
-- Bytes waeren weg, bevor sie jemand gelesen hat. Deshalb:
|
||||
-- Stufe 1 (diese Migration): "storagePath" dazu (NULLbar), "data" bleibt
|
||||
-- stehen und wird NULLbar, damit neue Uploads sie leer lassen.
|
||||
-- Stufe 2 (spaetere Freigabe, Migration
|
||||
-- 20260922120100_dashboard_image_drop_data, vorgemerkt in
|
||||
-- .planning/todos/pending/): "storagePath" SET NOT NULL und
|
||||
-- DROP COLUMN "data" — erst, wenn alpha UND live einmal mit
|
||||
-- einer Version >= dieser gelaufen sind.
|
||||
--
|
||||
-- Das Prisma-Modell behaelt in Stufe 1 bewusst `data Bytes?` (optional).
|
||||
-- Damit bleibt der Bootstrap-Umzug typisiert und braucht kein rohes SQL;
|
||||
-- die Spalte verschwindet aus Modell und Tabelle gemeinsam in Stufe 2.
|
||||
--
|
||||
-- Rechte/Regeln: "tenant_isolation_policy" aus 20260921120000 bleibt
|
||||
-- unveraendert. Kein DROP POLICY.
|
||||
|
||||
-- Relativer Pfad zur Monorepo-Wurzel, z. B.
|
||||
-- "user-files/dashboard-images/<userId>/<id>.png". Stufe 2 macht die Spalte
|
||||
-- NOT NULL.
|
||||
ALTER TABLE "DashboardImage" ADD COLUMN "storagePath" TEXT;
|
||||
|
||||
-- Neue Uploads schreiben keine Bytes mehr in die Zeile; die Spalte bleibt
|
||||
-- fuer die Dauer von Stufe 1 als Sicherheitsnetz erhalten.
|
||||
ALTER TABLE "DashboardImage" ALTER COLUMN "data" DROP NOT NULL;
|
||||
|
||||
-- Systemkontext-Leserecht (Muster 20260914120000_rls_system_context_read):
|
||||
-- der Bootstrap-Umzug liest die noch nicht umgezogenen Zeilen ueber ALLE
|
||||
-- Mandanten (`forSystem()`), bevor er je Zeile mandantengebunden
|
||||
-- zurueckschreibt. Ohne diese Regel saehe er nach dem Scharfschalten der
|
||||
-- Datenbankrolle (Etappe 4, Schalter heute AUS) NULL Zeilen und stellte die
|
||||
-- Arbeit stumm ein — genau die Falle, die 20260914120000 fuer die fuenf
|
||||
-- Hintergrunddienst-Tabellen geschlossen hat. Permissiv und NUR FOR SELECT:
|
||||
-- Schreiben bleibt allein der Mandantenregel unterstellt.
|
||||
CREATE POLICY system_read_policy ON "DashboardImage"
|
||||
FOR SELECT USING (is_system_context());
|
||||
@@ -212,12 +212,16 @@ model WidgetInstance {
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
// Bilderrahmen-Widget (quick-260921-pi9): hochgeladene Bilder eines Benutzers,
|
||||
// als bytea in der Datenbank (kein Docker-Volume, die Sicherung deckt es mit
|
||||
// ab). Keine Relation — wie WidgetInstance. Grenzen (5 MiB je Datei, 30 je
|
||||
// Bilderrahmen-Widget (quick-260921-pi9): hochgeladene Bilder eines Benutzers.
|
||||
// Keine Relation — wie WidgetInstance. Grenzen (5 MiB je Datei, 30 je
|
||||
// Benutzer) und die Magic-Byte-Erkennung leben in
|
||||
// src/dashboard/dashboard-image-rules.ts; Besitz = gleicher Mandant UND
|
||||
// gleicher Benutzer (Regel in Migration 20260921120000 mit Benutzerdimension).
|
||||
//
|
||||
// quick-260922-hk4: die Bytes liegen jetzt im Dateibereich
|
||||
// (user-files/dashboard-images/<userId>/<id>.<ext>), die Zeile haelt nur
|
||||
// noch den relativen Pfad — Muster User.avatarPath. Die Datenbanksicherung
|
||||
// (pg_dump) bleibt dadurch klein.
|
||||
model DashboardImage {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
@@ -225,7 +229,14 @@ model DashboardImage {
|
||||
originalName String
|
||||
mimeType String
|
||||
size Int
|
||||
data Bytes
|
||||
// Stufe 1 der zweistufigen Umstellung (Migration 20260922120000): die
|
||||
// Spalte bleibt NULLbar stehen, bis der Bootstrap-Umzug auf allen Servern
|
||||
// gelaufen ist. Neue Uploads schreiben sie nie. DROP kommt mit
|
||||
// 20260922120100 (vorgemerkt in .planning/todos/pending/).
|
||||
data Bytes?
|
||||
// Relativ zur Monorepo-Wurzel; NULL nur fuer Zeilen, die der
|
||||
// Bootstrap-Umzug noch nicht angefasst hat. Wird in Stufe 2 NOT NULL.
|
||||
storagePath String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
|
||||
Reference in New Issue
Block a user