4b9214dbaf
getCmsFileUrl() returned /cms-files?src=... for all URLs including external ones. resolveCmsSource() rejects foreign hosts (SSRF guard), so external file links 404'd. External http(s) URLs now returned directly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
103 lines
3.0 KiB
TypeScript
103 lines
3.0 KiB
TypeScript
/**
|
|
* 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/...`).
|
|
* Externe http(s)-URLs werden direkt zurückgegeben — der /cms-files-Proxy
|
|
* würde sie wegen SSRF-Schutz sowieso ablehnen.
|
|
*/
|
|
export function getCmsFileUrl(src: string, opts: CmsFileUrlOptions = {}): string {
|
|
if (src.startsWith('http://') || src.startsWith('https://')) {
|
|
return src;
|
|
}
|
|
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;
|
|
}
|
|
}
|