// 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"; const DEFAULT_TTL = 1000 * 60 * 15; // 15min const EXTENDED_TTL = 1000 * 60 * 60 * 24 * 7; // 1 week // 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"); } // 1. Create a unique cache key for this specific asset variation const cacheKey = `asset:${path}?w=${width ?? ""}&h=${height ?? ""}`; const ttl = path.toString().includes("cover") ? DEFAULT_TTL : EXTENDED_TTL; 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 const pathsToTry = [path]; if (!path.startsWith("_")) { pathsToTry.push(`_${path}`); } let lastError: any = null; for (const currentPath of pathsToTry) { try { // A. Try Nextcloud Preview (Resizing) if (width || height) { 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; } const isJpg = fullPath.match(/\.(jpg|jpeg)$/i); const previewExtension = isJpg ? "jpg" : "png"; const previewUrl = new URL( `${baseUrl}index.php/core/preview.${previewExtension}`, ); 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"); const auth = Buffer.from( `${env.NEXTCLOUD_USER}:${env.NEXTCLOUD_PASSWORD}`, ).toString("base64"); 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"); // --- END OF FETCH LOGIC --- }, ttl, ); // 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"); } };