Bilder, die vor der Umstellung hochgeladen wurden, liegen unverkleinert im Originalformat im Repo - damals war sharp im Container kaputt, es gab also weder Verkleinerung noch Formatwandlung. Das Skript schickt sie durch dieselbe Verarbeitung wie einen frischen Upload. node dist/scripts/convert-images-to-avif.js # nur anzeigen node dist/scripts/convert-images-to-avif.js --apply # wirklich tun Ohne --apply wird nichts geschrieben. Ablauf: Workspace auf den Repo-Stand bringen, jedes referenzierte Bild umwandeln und nach seinem Inhalt benennen (Event-Titel, Alt-Text, Zweck), die Verweise in Events, Gallery und Textbereichen nachziehen, die Vorgaenger wegraeumen und committen. Weggeraeumt wird nur, was das CMS selbst angelegt hat. Ein handgepflegtes event_karaoke.jpg bekommt zwar eine AVIF-Fassung und der Verweis zeigt darauf, das Original bleibt aber liegen und wird im Bericht genannt. saveImageBuffer aus upload.service.ts herausgeloest, damit Upload und Skript nachweislich dieselbe Verarbeitung benutzen statt zweier Kopien. replaceImageUrls ersetzt Pfade in den Textbereichen, ohne die restliche JSON-Struktur anzufassen. Durchgespielt an vier Bildern zu je ~656 KB: 2.56 MB -> 0.16 MB. Die Vorschau schreibt nichts, ein zweiter Lauf findet nichts mehr, die Highlights-Liste bleibt unversehrt und alle Bilder werden danach als image/avif ausgeliefert. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
129 lines
3.7 KiB
TypeScript
129 lines
3.7 KiB
TypeScript
import { db } from '../config/database.js';
|
|
import { events, galleryImages, contentSections } from '../db/schema.js';
|
|
import { AssetService } from './asset.service.js';
|
|
import { isManagedAsset, forgetManagedAsset } from './managed-assets.service.js';
|
|
|
|
const assets = new AssetService();
|
|
|
|
/**
|
|
* Loescht eine Bilddatei, sofern das CMS sie selbst angelegt hat und kein
|
|
* Datensatz sie mehr benutzt. Muss NACH dem Loeschen bzw. Aktualisieren der
|
|
* Zeile aufgerufen werden.
|
|
*/
|
|
export async function dropImageIfUnused(url: string | null | undefined): Promise<boolean> {
|
|
if (!url) return false;
|
|
if (!(await isManagedAsset(url))) return false;
|
|
|
|
const referenced = await collectReferencedImageUrls();
|
|
if (referenced.has(url)) return false;
|
|
|
|
const deleted = assets.deleteImage(url);
|
|
await forgetManagedAsset(url);
|
|
return deleted;
|
|
}
|
|
|
|
/**
|
|
* Entfernt alle vom CMS angelegten Bilder, die nirgends mehr referenziert
|
|
* werden. Die Referenzliste deckt bewusst ALLE Zeilen ab, auch
|
|
* unveroeffentlichte - sonst verlieren die ihr Bild.
|
|
*/
|
|
export async function sweepOrphanedImages(): Promise<string[]> {
|
|
const referenced = await collectReferencedImageUrls();
|
|
const removed: string[] = [];
|
|
|
|
for (const url of assets.listImageFiles()) {
|
|
if (referenced.has(url)) continue;
|
|
if (!(await isManagedAsset(url))) continue;
|
|
|
|
if (assets.deleteImage(url)) removed.push(url);
|
|
await forgetManagedAsset(url);
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Ersetzt Bildpfade in einem beliebigen Content-JSON anhand einer Zuordnung
|
|
* alt -> neu. Die Struktur bleibt dabei unveraendert.
|
|
*/
|
|
export function replaceImageUrls(value: any, mapping: Map<string, string>): any {
|
|
if (typeof value === 'string') {
|
|
return mapping.get(value) ?? value;
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
return value.map((entry) => replaceImageUrls(entry, mapping));
|
|
}
|
|
|
|
if (value && typeof value === 'object') {
|
|
const out: Record<string, any> = {};
|
|
for (const [key, entry] of Object.entries(value)) {
|
|
out[key] = replaceImageUrls(entry, mapping);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/** 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);
|
|
}
|
|
}
|