Bilder wurden nie geloescht - weder beim Loeschen eines Datensatzes noch beim Austauschen. Dazu waren die Textbereiche zwar im Backend vorhanden, aber ohne Oberflaeche, und die Getraenkekarte hing hartkodiert im Markup. Aufraeumen: - asset.service.ts loescht ausschliesslich Dateien, die das CMS selbst angelegt hat (Muster <zeitstempel>-<zufall>.<ext>) und nur innerhalb der Upload-Ordner. Handgepflegte Assets wie event_karaoke.jpg oder Welcome.png bleiben unangetastet, auch wenn sie nirgends referenziert sind. - image-refs.service.ts sammelt alle benutzten Bild-URLs, inklusive der Pfade aus den Textbereichen. Geloescht wird nur, was wirklich niemand mehr benutzt - ein von zwei Events geteiltes Bild bleibt liegen. - Events und Gallery raeumen beim Loeschen und beim Bildwechsel mit auf. - Der Publish entfernt zusaetzlich Verwaiste. Die Referenzliste deckt bewusst alle Zeilen ab, auch unveroeffentlichte, sonst verlieren die ihr Bild. Die Loeschungen werden mitcommittet, das Repo schrumpft also. Texte: - Neuer Adminbereich fuer Hero, Willkommen und Drinks ueber die schon vorhandenen /api/content-Endpunkte, samt Highlights-Liste und Bildern. - Der Generator maskiert Texte jetzt. Ohne das haette ein "<" oder "&" in einem Feld gereicht, um den Astro-Build und damit den Deploy zu killen. PDF: - POST /api/pdf/drinks nimmt die Getraenkekarte entgegen, hinterlegt die URL in der drinks-Section und raeumt den Vorgaenger weg. Der Generator verlinkt sie, statt den Pfad fest im Markup zu haben. Ausserdem: - Vorschaubilder im Admin auf eine feste Box gezwungen. Uploads sind bis 1600px breit und haben das Layout je nach Seitenverhaeltnis zerrissen. - git.service.ts sichert public/pdf beim Neu-Clone mit weg, nicht nur public/images. - Der Publish scheitert nicht mehr, wenn nur das Audit-Log nicht geschrieben werden kann - der Push ist da laengst durch. - Upload-Logik lag dreifach kopiert vor, jetzt in upload.service.ts. Der Helfer prueft auch auf abgeschnittene Dateien; bisher landete bei zu grossen Uploads ein kaputtes Bild auf der Platte. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
78 lines
2.3 KiB
TypeScript
78 lines
2.3 KiB
TypeScript
import { db } from '../config/database.js';
|
|
import { events, galleryImages, contentSections } from '../db/schema.js';
|
|
import { AssetService } from './asset.service.js';
|
|
|
|
const assets = new AssetService();
|
|
|
|
/**
|
|
* Loescht eine Bilddatei, sofern sie von keinem Datensatz mehr benutzt wird.
|
|
* Muss NACH dem Loeschen bzw. Aktualisieren der Zeile aufgerufen werden.
|
|
*/
|
|
export async function dropImageIfUnused(url: string | null | undefined): Promise<boolean> {
|
|
if (!url) return false;
|
|
const referenced = await collectReferencedImageUrls();
|
|
if (referenced.has(url)) return false;
|
|
return assets.deleteImage(url);
|
|
}
|
|
|
|
/**
|
|
* Sammelt jede Bild-URL, die irgendwo in der Datenbank vorkommt.
|
|
*
|
|
* Bewusst ueber ALLE Zeilen, nicht nur die veroeffentlichten - sonst wuerde
|
|
* ein unveroeffentlichtes Event sein Bild verlieren, sobald jemand publisht.
|
|
*
|
|
* Die Content-Sections enthalten beliebiges JSON (Welcome-Bild, Monatshit,
|
|
* Whiskey-Bilder), deshalb wird es rekursiv nach Bildpfaden durchsucht.
|
|
*/
|
|
export async function collectReferencedImageUrls(): Promise<Set<string>> {
|
|
const urls = new Set<string>();
|
|
|
|
for (const row of (await db.select().from(events)) as any[]) {
|
|
if (row.imageUrl) urls.add(row.imageUrl);
|
|
}
|
|
|
|
for (const row of (await db.select().from(galleryImages)) as any[]) {
|
|
if (row.imageUrl) urls.add(row.imageUrl);
|
|
}
|
|
|
|
for (const row of (await db.select().from(contentSections)) as any[]) {
|
|
collectFromJson(row.contentJson, urls);
|
|
}
|
|
|
|
return urls;
|
|
}
|
|
|
|
/** Alle Bildpfade aus einem beliebigen Content-JSON. */
|
|
export function extractImageUrls(value: any): Set<string> {
|
|
const out = new Set<string>();
|
|
collectFromJson(value, out);
|
|
return out;
|
|
}
|
|
|
|
function collectFromJson(value: any, out: Set<string>): void {
|
|
if (typeof value === 'string') {
|
|
if (value.startsWith('/images/')) {
|
|
out.add(value);
|
|
return;
|
|
}
|
|
// Je nach Treiber kommt das JSON als String zurueck
|
|
if (value.startsWith('{') || value.startsWith('[')) {
|
|
try {
|
|
collectFromJson(JSON.parse(value), out);
|
|
} catch {
|
|
// kein JSON - ignorieren
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
for (const entry of value) collectFromJson(entry, out);
|
|
return;
|
|
}
|
|
|
|
if (value && typeof value === 'object') {
|
|
for (const entry of Object.values(value)) collectFromJson(entry, out);
|
|
}
|
|
}
|