feat: add FilesBlock interface and component integration
- 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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user