import fs from 'fs'; import path from 'path'; import { env } from '../config/env.js'; /** * Kennt die Verzeichnisse, in die das CMS schreibt, und sorgt dafuer, dass * kein Pfad ausserhalb davon angefasst wird. * * Ob eine konkrete Datei geloescht werden DARF, entscheidet diese Klasse * bewusst nicht - das steht in managed-assets.service.ts. */ // Unterordner unterhalb von public/ export const MANAGED_IMAGE_DIRS = ['images/events', 'images/gallery', 'images/content']; export const MANAGED_PDF_DIR = 'pdf'; export class AssetService { private publicDir: string; constructor() { this.publicDir = path.join(env.GIT_WORKSPACE_DIR, 'public'); } /** Absoluter Pfad im public-Verzeichnis, oder null wenn ausserhalb. */ private resolveInPublic(url: string): string | null { if (!url || typeof url !== 'string' || !url.startsWith('/')) return null; const relative = url.replace(/^\/+/, '').split('?')[0].split('#')[0]; const absolute = path.resolve(this.publicDir, relative); // Traversal-Schutz: muss unterhalb von public/ bleiben if (absolute !== this.publicDir && !absolute.startsWith(this.publicDir + path.sep)) { return null; } return absolute; } /** Absoluter Pfad, sofern die Datei in einem der erlaubten Ordner liegt. */ resolveInDirs(url: string, dirs: string[]): string | null { const absolute = this.resolveInPublic(url); if (!absolute) return null; const relative = path.relative(this.publicDir, absolute); const dir = path.dirname(relative).split(path.sep).join('/'); return dirs.includes(dir) ? absolute : null; } /** Existiert die Datei bereits? Fuer die Namensvergabe. */ exists(url: string, dirs: string[]): boolean { const absolute = this.resolveInDirs(url, dirs); return absolute ? fs.existsSync(absolute) : false; } /** Loescht eine Bilddatei. Die Besitzfrage muss vorher geklaert sein. */ deleteImage(url: string): boolean { return this.unlink(this.resolveInDirs(url, MANAGED_IMAGE_DIRS)); } /** Loescht ein PDF. Die Besitzfrage muss vorher geklaert sein. */ deletePdf(url: string): boolean { return this.unlink(this.resolveInDirs(url, [MANAGED_PDF_DIR])); } private unlink(absolute: string | null): boolean { if (!absolute) return false; try { fs.unlinkSync(absolute); return true; } catch (err: any) { if (err?.code === 'ENOENT') return false; throw err; } } /** Alle Bilddateien, die in den verwalteten Ordnern liegen, als URL-Pfade. */ listImageFiles(): string[] { const found: string[] = []; for (const dir of MANAGED_IMAGE_DIRS) { const absoluteDir = path.join(this.publicDir, dir); if (!fs.existsSync(absoluteDir)) continue; for (const name of fs.readdirSync(absoluteDir)) { const absolute = path.join(absoluteDir, name); if (!fs.statSync(absolute).isFile()) continue; found.push(`/${dir}/${name}`); } } return found; } }