/** * Datei-URLs für SvelteKit: /cms-files mit persistentem Server-Disk-Cache. * Analog zu rusty-image.ts, aber für generische Dateien (PDF, DOCX, ...). */ export interface CmsFileField { url: string; title?: string; description?: string; size?: number; mime?: string; filename?: string; } /** * Extrahiert URL/Title/Description aus einem CMS-File-Field * (string oder { src, title?, description?, mime?, size?, ... }). */ export function extractCmsFileField(field: unknown): CmsFileField | null { if (typeof field === 'string') { const s = field.trim(); if (!s) return null; return { url: s.startsWith('//') ? `https:${s}` : s }; } if (field && typeof field === 'object') { const o = field as { src?: string; file?: { url?: string }; title?: string; description?: string; size?: number; mime?: string; filename?: string; }; const rawUrl = typeof o.src === 'string' && o.src.trim() ? o.src : typeof o.file?.url === 'string' && o.file.url.trim() ? o.file.url : undefined; if (!rawUrl) return null; const url = rawUrl.startsWith('//') ? `https:${rawUrl}` : rawUrl; return { url, ...(o.title ? { title: o.title } : {}), ...(o.description ? { description: o.description } : {}), ...(typeof o.size === 'number' ? { size: o.size } : {}), ...(o.mime ? { mime: o.mime } : {}), ...(o.filename ? { filename: o.filename } : {}), }; } return null; } export interface CmsFileUrlOptions { /** Filename hint for Content-Disposition header. */ filename?: string; /** Force download (Content-Disposition: attachment). */ download?: boolean; } /** * Lokale Datei-URL: /cms-files?src=…&name=…&dl=… * `src` = volle Datei-URL oder CMS-Asset-Pfad (`/api/assets/...`). */ export function getCmsFileUrl(src: string, opts: CmsFileUrlOptions = {}): string { const params = new URLSearchParams(); params.set('src', src); if (opts.filename) params.set('name', opts.filename); if (opts.download) params.set('dl', '1'); return `/cms-files?${params.toString()}`; } const SIZE_UNITS = ['B', 'KB', 'MB', 'GB']; export function formatFileSize(bytes?: number): string | null { if (typeof bytes !== 'number' || !Number.isFinite(bytes) || bytes <= 0) return null; let b = bytes; let i = 0; while (b >= 1024 && i < SIZE_UNITS.length - 1) { b /= 1024; i++; } const rounded = b >= 10 || i === 0 ? Math.round(b) : Math.round(b * 10) / 10; return `${rounded} ${SIZE_UNITS[i]}`; } export function fileExtFromUrl(url: string): string | null { try { const path = url.split('?')[0].split('#')[0]; const last = path.split('/').pop() ?? ''; const dot = last.lastIndexOf('.'); return dot >= 0 ? last.slice(dot + 1).toLowerCase() : null; } catch { return null; } }