From df9e6e95cbfd6140e17beb0b06aa9af003c8bb02 Mon Sep 17 00:00:00 2001 From: u80864958 Date: Fri, 23 Jan 2026 13:50:40 +0100 Subject: [PATCH] feat: cache images --- src/app.html | 10 +- src/lib/server/cache.ts | 42 +++--- src/routes/[[lang=lang]]/+page.svelte | 5 +- src/routes/assets/[...path]/+server.ts | 187 ++++++++++++++----------- 4 files changed, 140 insertions(+), 104 deletions(-) diff --git a/src/app.html b/src/app.html index 6c747a3..29518d9 100644 --- a/src/app.html +++ b/src/app.html @@ -2,11 +2,11 @@ - + + + + + schreifuchs.ch diff --git a/src/lib/server/cache.ts b/src/lib/server/cache.ts index b839855..ec4c41d 100644 --- a/src/lib/server/cache.ts +++ b/src/lib/server/cache.ts @@ -7,37 +7,41 @@ const DEFAULT_TTL = 1000 * 60 * 15; // 15 minutes default TTL * @param fetcher Function that returns a promise with the data to cache. * @param ttl Time to live in milliseconds. Defaults to 15 minutes. */ -export async function withCache(key: string, fetcher: () => Promise, ttl = DEFAULT_TTL): Promise { - const now = Date.now(); - const entry = cache.get(key); +export async function withCache( + key: string, + fetcher: () => Promise, + ttl = DEFAULT_TTL, +): Promise { + const now = Date.now(); + const entry = cache.get(key); - if (entry && (now - entry.timestamp < ttl)) { - return entry.data as T; - } + if (entry && now - entry.timestamp < ttl) { + return entry.data as T; + } - try { - const data = await fetcher(); - cache.set(key, { timestamp: now, data }); - return data; - } catch (error) { - console.error(`Error fetching data for key ${key}:`, error); - // If fetch fails but we have stale data, return it? - // For now, let's bubble the error up so the page handles it (e.g. 404), - // or maybe just re-throw. - throw error; - } + try { + const data = await fetcher(); + cache.set(key, { timestamp: now, data }); + return data; + } catch (error) { + console.error(`Error fetching data for key ${key}:`, error); + // If fetch fails but we have stale data, return it? + // For now, let's bubble the error up so the page handles it (e.g. 404), + // or maybe just re-throw. + throw error; + } } /** * Manually clears a specific cache key. */ export function clearCache(key: string) { - cache.delete(key); + cache.delete(key); } /** * Clears the entire cache. */ export function clearAllCache() { - cache.clear(); + cache.clear(); } diff --git a/src/routes/[[lang=lang]]/+page.svelte b/src/routes/[[lang=lang]]/+page.svelte index 2e52eac..209b927 100644 --- a/src/routes/[[lang=lang]]/+page.svelte +++ b/src/routes/[[lang=lang]]/+page.svelte @@ -23,7 +23,7 @@ // Helper to resolve image source: Local override -> Nextcloud cover -> Placeholder (handled by ImageTile) const getTileImage = (page: { slug: string; coverImage: string | null }) => { - return staticImages[page.slug.toLowerCase()] || page.coverImage || ""; + return page.coverImage || staticImages[page.slug.toLowerCase()] || ""; }; @@ -46,4 +46,5 @@ {m.pfadi()} - \ No newline at end of file + + diff --git a/src/routes/assets/[...path]/+server.ts b/src/routes/assets/[...path]/+server.ts index d196a69..446c199 100644 --- a/src/routes/assets/[...path]/+server.ts +++ b/src/routes/assets/[...path]/+server.ts @@ -1,101 +1,132 @@ +// src/routes/assets/[...path]/+server.ts import { client } from "$lib/server/nextcloud"; import { error } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; import mime from "mime-types"; import { env } from "$env/dynamic/private"; +import { withCache } from "$lib/server/cache"; + +// Define what we are storing in the cache +type CachedAsset = { + buffer: ArrayBuffer; + contentType: string; +}; export const GET: RequestHandler = async ({ params, url }) => { - const { path } = params; - const width = url.searchParams.get("w"); - const height = url.searchParams.get("h"); - - if (!path) { - throw error(400, "Path is required"); - } + const { path } = params; + const width = url.searchParams.get("w"); + const height = url.searchParams.get("h"); - try { - let buffer: ArrayBuffer; - let contentType: string; + if (!path) { + throw error(400, "Path is required"); + } + + // 1. Create a unique cache key for this specific asset variation + const cacheKey = `asset:${path}?w=${width ?? ""}&h=${height ?? ""}`; + + try { + // 2. Use withCache to wrap the fetching logic + const cachedData = await withCache( + cacheKey, + async () => { + // --- START OF FETCH LOGIC --- + // This function only runs if the data is NOT in the cache - // Try both original path and hidden variant if applicable const pathsToTry = [path]; - if (!path.startsWith('_')) { - pathsToTry.push(`_${path}`); + if (!path.startsWith("_")) { + pathsToTry.push(`_${path}`); } - - let response: Response | null = null; let lastError: any = null; for (const currentPath of pathsToTry) { - try { - if (width || height) { - // Use Nextcloud Preview API - const baseUrl = env.NEXTCLOUD_URL?.endsWith("/") ? env.NEXTCLOUD_URL : `${env.NEXTCLOUD_URL}/`; - - let fullPath = currentPath; - if (env.NEXTCLOUD_BASE_DIR) { - const baseDir = env.NEXTCLOUD_BASE_DIR.replace(/^\/+|\/+$/g, ""); - fullPath = baseDir ? `${baseDir}/${currentPath}` : currentPath; - } + try { + // A. Try Nextcloud Preview (Resizing) + if (width || height) { + const baseUrl = env.NEXTCLOUD_URL?.endsWith("/") + ? env.NEXTCLOUD_URL + : `${env.NEXTCLOUD_URL}/`; - const isJpg = fullPath.match(/\.(jpg|jpeg)$/i); - const previewExtension = isJpg ? 'jpg' : 'png'; - const previewUrl = new URL(`${baseUrl}index.php/core/preview.${previewExtension}`); - // Ensure leading slash for the file parameter - const fileParam = fullPath.startsWith('/') ? fullPath : `/${fullPath}`; - previewUrl.searchParams.set("file", fileParam); - if (width) previewUrl.searchParams.set("x", width); - if (height) previewUrl.searchParams.set("y", height); - previewUrl.searchParams.set("forceIcon", "0"); + let fullPath = currentPath; + if (env.NEXTCLOUD_BASE_DIR) { + const baseDir = env.NEXTCLOUD_BASE_DIR.replace( + /^\/+|\/+$/g, + "", + ); + fullPath = baseDir ? `${baseDir}/${currentPath}` : currentPath; + } - const auth = Buffer.from(`${env.NEXTCLOUD_USER}:${env.NEXTCLOUD_PASSWORD}`).toString("base64"); - - console.log(`Fetching preview: ${previewUrl.toString()} for file: ${fullPath}`); + const isJpg = fullPath.match(/\.(jpg|jpeg)$/i); + const previewExtension = isJpg ? "jpg" : "png"; + const previewUrl = new URL( + `${baseUrl}index.php/core/preview.${previewExtension}`, + ); - const ncResponse = await fetch(previewUrl.toString(), { - headers: { - "Authorization": `Basic ${auth}` - } - }); + const fileParam = fullPath.startsWith("/") + ? fullPath + : `/${fullPath}`; + previewUrl.searchParams.set("file", fileParam); + if (width) previewUrl.searchParams.set("x", width); + if (height) previewUrl.searchParams.set("y", height); + previewUrl.searchParams.set("forceIcon", "0"); - if (ncResponse.ok) { - buffer = await ncResponse.arrayBuffer(); - contentType = ncResponse.headers.get("Content-Type") || "image/png"; - - return new Response(buffer, { - headers: { - "Content-Type": contentType, - "Cache-Control": "public, max-age=86400", - "Vary": "Accept-Encoding" - } - }); - } else { - console.warn(`Nextcloud preview failed (${ncResponse.status}) for ${fullPath}.`); - } - } + const auth = Buffer.from( + `${env.NEXTCLOUD_USER}:${env.NEXTCLOUD_PASSWORD}`, + ).toString("base64"); - // Fetch original via WebDAV (Fallback or no resize requested) - console.log(`Fetching original file via WebDAV: /${currentPath}`); - const data = await client.getFileContents("/" + currentPath); - buffer = data as ArrayBuffer; - contentType = mime.lookup(currentPath) || "application/octet-stream"; - - return new Response(buffer, { - headers: { - "Content-Type": contentType, - "Cache-Control": "public, max-age=86400", - "Vary": "Accept-Encoding" - } - }); - } catch (e) { - lastError = e; - continue; // Try next path variant + console.log( + `Fetching preview: ${previewUrl.toString()} for file: ${fullPath}`, + ); + + const ncResponse = await fetch(previewUrl.toString(), { + headers: { Authorization: `Basic ${auth}` }, + }); + + if (ncResponse.ok) { + const buffer = await ncResponse.arrayBuffer(); + const contentType = + ncResponse.headers.get("Content-Type") || "image/png"; + + // Return the raw data to be cached + return { buffer, contentType }; + } else { + console.warn( + `Nextcloud preview failed (${ncResponse.status}) for ${fullPath}.`, + ); + } } + + // B. Try WebDAV (Original) + console.log(`Fetching original file via WebDAV: /${currentPath}`); + const data = await client.getFileContents("/" + currentPath); + const buffer = data as ArrayBuffer; + const contentType = + mime.lookup(currentPath) || "application/octet-stream"; + + // Return the raw data to be cached + return { buffer, contentType }; + } catch (e) { + lastError = e; + continue; // Try next path variant + } } - + throw lastError || new Error("Asset not found"); - } catch (e) { - console.error(`Error fetching asset from Nextcloud: ${path}`, e); - throw error(404, "Asset not found"); - } + // --- END OF FETCH LOGIC --- + }, + // Optional: Set a specific TTL for images (e.g., 1 hour instead of default 15m) + 1000 * 60 * 60, + ); + + // 3. Construct a fresh Response using the cached data + return new Response(cachedData.buffer, { + headers: { + "Content-Type": cachedData.contentType, + "Cache-Control": "public, max-age=86400", // Browser cache + Vary: "Accept-Encoding", + }, + }); + } catch (e) { + console.error(`Error fetching asset: ${path}`, e); + throw error(404, "Asset not found"); + } };