feat: cache images
This commit is contained in:
+5
-5
@@ -2,11 +2,11 @@
|
||||
<html lang="%paraglide.lang%" dir="%paraglide.textDirection%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script
|
||||
defer
|
||||
src="https://umami.schreifuchs.ch/script.js"
|
||||
data-website-id="f09f9cc5-6357-48df-8fb2-5c62089c04a0"
|
||||
></script>
|
||||
<!-- <script -->
|
||||
<!-- defer -->
|
||||
<!-- src="https://umami.schreifuchs.ch/script.js" -->
|
||||
<!-- data-website-id="f09f9cc5-6357-48df-8fb2-5c62089c04a0" -->
|
||||
<!-- ></script> -->
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>schreifuchs.ch</title>
|
||||
|
||||
@@ -7,11 +7,15 @@ 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<T>(key: string, fetcher: () => Promise<T>, ttl = DEFAULT_TTL): Promise<T> {
|
||||
export async function withCache<T>(
|
||||
key: string,
|
||||
fetcher: () => Promise<T>,
|
||||
ttl = DEFAULT_TTL,
|
||||
): Promise<T> {
|
||||
const now = Date.now();
|
||||
const entry = cache.get(key);
|
||||
|
||||
if (entry && (now - entry.timestamp < ttl)) {
|
||||
if (entry && now - entry.timestamp < ttl) {
|
||||
return entry.data as T;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()] || "";
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -47,3 +47,4 @@
|
||||
</h2>
|
||||
</ImageLinkTile>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
// 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;
|
||||
@@ -13,80 +21,89 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
||||
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 {
|
||||
let buffer: ArrayBuffer;
|
||||
let contentType: string;
|
||||
// 2. Use withCache to wrap the fetching logic
|
||||
const cachedData = await withCache<CachedAsset>(
|
||||
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('_')) {
|
||||
if (!path.startsWith("_")) {
|
||||
pathsToTry.push(`_${path}`);
|
||||
}
|
||||
|
||||
let response: Response | null = null;
|
||||
let lastError: any = null;
|
||||
|
||||
for (const currentPath of pathsToTry) {
|
||||
try {
|
||||
// A. Try Nextcloud Preview (Resizing)
|
||||
if (width || height) {
|
||||
// Use Nextcloud Preview API
|
||||
const baseUrl = env.NEXTCLOUD_URL?.endsWith("/") ? env.NEXTCLOUD_URL : `${env.NEXTCLOUD_URL}/`;
|
||||
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, "");
|
||||
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}`);
|
||||
// Ensure leading slash for the file parameter
|
||||
const fileParam = fullPath.startsWith('/') ? fullPath : `/${fullPath}`;
|
||||
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");
|
||||
const auth = Buffer.from(
|
||||
`${env.NEXTCLOUD_USER}:${env.NEXTCLOUD_PASSWORD}`,
|
||||
).toString("base64");
|
||||
|
||||
console.log(`Fetching preview: ${previewUrl.toString()} for file: ${fullPath}`);
|
||||
console.log(
|
||||
`Fetching preview: ${previewUrl.toString()} for file: ${fullPath}`,
|
||||
);
|
||||
|
||||
const ncResponse = await fetch(previewUrl.toString(), {
|
||||
headers: {
|
||||
"Authorization": `Basic ${auth}`
|
||||
}
|
||||
headers: { Authorization: `Basic ${auth}` },
|
||||
});
|
||||
|
||||
if (ncResponse.ok) {
|
||||
buffer = await ncResponse.arrayBuffer();
|
||||
contentType = ncResponse.headers.get("Content-Type") || "image/png";
|
||||
const buffer = await ncResponse.arrayBuffer();
|
||||
const 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"
|
||||
}
|
||||
});
|
||||
// Return the raw data to be cached
|
||||
return { buffer, contentType };
|
||||
} else {
|
||||
console.warn(`Nextcloud preview failed (${ncResponse.status}) for ${fullPath}.`);
|
||||
console.warn(
|
||||
`Nextcloud preview failed (${ncResponse.status}) for ${fullPath}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch original via WebDAV (Fallback or no resize requested)
|
||||
// B. Try WebDAV (Original)
|
||||
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";
|
||||
const buffer = data as ArrayBuffer;
|
||||
const contentType =
|
||||
mime.lookup(currentPath) || "application/octet-stream";
|
||||
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"Cache-Control": "public, max-age=86400",
|
||||
"Vary": "Accept-Encoding"
|
||||
}
|
||||
});
|
||||
// Return the raw data to be cached
|
||||
return { buffer, contentType };
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
continue; // Try next path variant
|
||||
@@ -94,8 +111,22 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
||||
}
|
||||
|
||||
throw lastError || new Error("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 from Nextcloud: ${path}`, e);
|
||||
console.error(`Error fetching asset: ${path}`, e);
|
||||
throw error(404, "Asset not found");
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user