feat: add FilesBlock interface and component integration
Deploy / verify (push) Successful in 46s
Deploy / deploy (push) Successful in 1m8s

- Introduced FilesBlockItem and FilesBlockData interfaces to support file handling in the CMS.
- Integrated FilesBlock component into ContentRows.svelte for rendering file blocks.
- Updated block type handling to include support for files, enhancing content versatility.
This commit is contained in:
Peter Meier
2026-04-22 10:23:20 +02:00
parent bedfe597ab
commit 5b5ec7c659
6 changed files with 481 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
/**
* Persistenter Disk-Cache für CMS-Dateien (PDF, DOCX, ...).
* Analog zu image-cache.server.ts: Datei einmal vom CMS holen, auf Disk
* speichern, danach direkt von dort ausliefern.
*/
import { createHash } from 'node:crypto';
import { existsSync, mkdirSync } from 'node:fs';
import { readFile, readdir, writeFile } from 'node:fs/promises';
import { extname, join } from 'node:path';
const CACHE_DIR = process.env.FILE_CACHE_DIR?.trim() || '.cache/files';
const MAX_MEMORY_ENTRIES = Number(process.env.FILE_CACHE_MAX_MEMORY) || 50;
export interface CachedFile {
buffer: Buffer;
contentType: string;
ext: string;
}
const memoryCache = new Map<string, CachedFile>();
let resolvedDir: string | null = null;
function ensureCacheDir(): string {
const dir = resolvedDir ?? join(process.cwd(), CACHE_DIR);
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
resolvedDir = dir;
return dir;
}
export function buildFileCacheKey(src: string): string {
return createHash('sha256').update(src).digest('hex').slice(0, 24);
}
const EXT_TO_CT: Record<string, string> = {
pdf: 'application/pdf',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
odt: 'application/vnd.oasis.opendocument.text',
ods: 'application/vnd.oasis.opendocument.spreadsheet',
zip: 'application/zip',
txt: 'text/plain; charset=utf-8',
csv: 'text/csv; charset=utf-8',
json: 'application/json',
xml: 'application/xml',
rtf: 'application/rtf',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
webp: 'image/webp',
gif: 'image/gif',
svg: 'image/svg+xml',
};
export function contentTypeFromExt(ext: string): string {
return EXT_TO_CT[ext.toLowerCase()] ?? 'application/octet-stream';
}
export function extFromContentType(ct: string): string | null {
const lower = ct.toLowerCase();
for (const [ext, mime] of Object.entries(EXT_TO_CT)) {
if (lower.startsWith(mime.split(';')[0])) return ext;
}
return null;
}
export function extFromSrc(src: string): string | null {
try {
const path = src.split('?')[0].split('#')[0];
const e = extname(path).slice(1).toLowerCase();
return e || null;
} catch {
return null;
}
}
function rememberInMemory(key: string, cached: CachedFile): CachedFile {
if (memoryCache.size >= MAX_MEMORY_ENTRIES) {
const oldest = memoryCache.keys().next().value;
if (oldest !== undefined) memoryCache.delete(oldest);
}
memoryCache.set(key, cached);
return cached;
}
export async function getCachedFile(key: string): Promise<CachedFile | null> {
const mem = memoryCache.get(key);
if (mem) return mem;
const dir = ensureCacheDir();
try {
const files = await readdir(dir);
const prefix = `${key}.`;
const name = files.find((f) => f.startsWith(prefix));
if (!name) return null;
const buf = await readFile(join(dir, name));
const ext = name.slice(prefix.length);
return rememberInMemory(key, {
buffer: buf,
contentType: contentTypeFromExt(ext),
ext,
});
} catch {
return null;
}
}
export async function cacheFile(
key: string,
buffer: ArrayBuffer | Buffer,
contentType: string,
srcHint?: string,
): Promise<CachedFile> {
const dir = ensureCacheDir();
const buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
const ext =
extFromContentType(contentType) ??
(srcHint ? extFromSrc(srcHint) : null) ??
'bin';
const cached: CachedFile = { buffer: buf, contentType, ext };
rememberInMemory(key, cached);
await writeFile(join(dir, `${key}.${ext}`), buf);
return cached;
}