feat(api): POST /api/cache/clear + flush images on asset webhooks
Deploy / verify (push) Failing after 38s
Deploy / deploy (push) Has been skipped

Two changes:
- New POST /api/cache/clear (auth: REVALIDATE_TOKEN). Wipes the
  TTL content cache, the image disk + memory cache, and forwards to
  the RustyCMS /api/transform/cache/invalidate endpoint when an
  API key is configured. Single trigger to flush everything.
- /api/revalidate now also calls clearImageCache() on asset.* events
  so a re-upload (focal change, alt edit, replacement) doesn't keep
  serving the previous transformed image from disk.

clearImageCache() in image-cache.server.ts wipes the in-memory map
and removes every file under IMAGE_CACHE_DIR.
This commit is contained in:
Peter Meier
2026-04-25 09:20:48 +02:00
parent 7264ad6707
commit 1ff4dc7a9a
3 changed files with 74 additions and 1 deletions
+24
View File
@@ -218,3 +218,27 @@ export function resolveExtension(params: ImageTransformParams, contentType?: str
if (contentType) return extForContentType(contentType);
return 'jpg';
}
/**
* Wipe in-memory + on-disk image cache. Returns counts cleared.
*/
export async function clearImageCache(): Promise<{ memory: number; disk: number }> {
const memory = memoryCache.size;
memoryCache.clear();
let disk = 0;
try {
const dir = ensureCacheDir();
const files = await readdir(dir);
for (const f of files) {
try {
await unlink(join(dir, f));
disk++;
} catch {
/* ignore */
}
}
} catch {
/* ignore */
}
return { memory, disk };
}
+45
View File
@@ -0,0 +1,45 @@
import type { RequestHandler } from './$types';
import { json, error } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
import { env as envPublic } from '$env/dynamic/public';
import { invalidateAll } from '$lib/cms';
import { clearImageCache } from '$lib/image-cache.server';
/**
* Manual full-cache purge:
* - SvelteKit-side TTL cache (cms.ts)
* - SvelteKit image cache (memory + disk)
* - Optionally: forwards POST to RustyCMS /api/transform/cache/invalidate
* when an API key is configured (RUSTYCMS_API_KEY env).
*
* Auth: same `REVALIDATE_TOKEN` as /api/revalidate.
*/
export const POST: RequestHandler = async ({ request, url }) => {
const token = env.REVALIDATE_TOKEN;
if (!token) throw error(500, 'REVALIDATE_TOKEN not configured');
const provided =
request.headers.get('x-revalidate-token') ??
request.headers.get('x-webhook-secret') ??
url.searchParams.get('token');
if (provided !== token) throw error(401, 'invalid token');
invalidateAll();
const img = await clearImageCache();
let cms: unknown = 'skipped';
const apiKey = env.RUSTYCMS_API_KEY;
const cmsUrl = (envPublic.PUBLIC_CMS_URL || '').replace(/\/$/, '');
if (apiKey && cmsUrl) {
try {
const res = await fetch(`${cmsUrl}/api/transform/cache/invalidate`, {
method: 'POST',
headers: { authorization: `Bearer ${apiKey}` },
});
cms = res.ok ? await res.json() : { error: res.status };
} catch (e) {
cms = { error: e instanceof Error ? e.message : String(e) };
}
}
return json({ ok: true, content_cache: 'cleared', image_cache: img, cms });
};
+5 -1
View File
@@ -2,6 +2,7 @@ import type { RequestHandler } from './$types';
import { json, error } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
import { invalidateAll, invalidateCollection } from '$lib/cms';
import { clearImageCache } from '$lib/image-cache.server';
/**
* RustyCMS Webhook Receiver.
@@ -50,7 +51,10 @@ export const POST: RequestHandler = async ({ request, url }) => {
if (event.startsWith('asset.')) {
// Assets referenziert überall → safe bet: purge all collections mit resolve
invalidateAll();
return json({ ok: true, purged: 'all', reason: event });
// Same image bytes (focal change, re-upload, alt edit) → image-cache must
// also flush so /cms-images stops serving the previous transform.
const img = await clearImageCache();
return json({ ok: true, purged: 'all', reason: event, image_cache: img });
}
if (collection) {