Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4e8baabdc | ||
|
|
ab3c8982e2 | ||
|
|
df9e6e95cb | ||
|
|
2154d6cfa8 | ||
|
|
9002f13f80 |
@@ -30,6 +30,7 @@
|
|||||||
"@inlang/paraglide-js": "^1.11.0",
|
"@inlang/paraglide-js": "^1.11.0",
|
||||||
"@inlang/paraglide-sveltekit": "^0.16.1",
|
"@inlang/paraglide-sveltekit": "^0.16.1",
|
||||||
"@ts-stack/markdown": "^1.5.0",
|
"@ts-stack/markdown": "^1.5.0",
|
||||||
|
"lru-cache": "^11.2.4",
|
||||||
"mime-types": "^3.0.2",
|
"mime-types": "^3.0.2",
|
||||||
"svelte-preprocess": "^6.0.2",
|
"svelte-preprocess": "^6.0.2",
|
||||||
"webdav": "^5.8.0"
|
"webdav": "^5.8.0"
|
||||||
|
|||||||
Generated
+9
@@ -20,6 +20,9 @@ importers:
|
|||||||
'@ts-stack/markdown':
|
'@ts-stack/markdown':
|
||||||
specifier: ^1.5.0
|
specifier: ^1.5.0
|
||||||
version: 1.5.0
|
version: 1.5.0
|
||||||
|
lru-cache:
|
||||||
|
specifier: ^11.2.4
|
||||||
|
version: 11.2.4
|
||||||
mime-types:
|
mime-types:
|
||||||
specifier: ^3.0.2
|
specifier: ^3.0.2
|
||||||
version: 3.0.2
|
version: 3.0.2
|
||||||
@@ -1269,6 +1272,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==}
|
resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==}
|
||||||
engines: {node: 14 || >=16.14}
|
engines: {node: 14 || >=16.14}
|
||||||
|
|
||||||
|
lru-cache@11.2.4:
|
||||||
|
resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
magic-string@0.30.10:
|
magic-string@0.30.10:
|
||||||
resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==}
|
resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==}
|
||||||
|
|
||||||
@@ -2988,6 +2995,8 @@ snapshots:
|
|||||||
|
|
||||||
lru-cache@10.2.2: {}
|
lru-cache@10.2.2: {}
|
||||||
|
|
||||||
|
lru-cache@11.2.4: {}
|
||||||
|
|
||||||
magic-string@0.30.10:
|
magic-string@0.30.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.4.15
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
|
|||||||
+9
-1
@@ -2,10 +2,18 @@
|
|||||||
<html lang="%paraglide.lang%" dir="%paraglide.textDirection%">
|
<html lang="%paraglide.lang%" dir="%paraglide.textDirection%">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
<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" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>schreifuchs.ch</title>
|
<title>schreifuchs.ch</title>
|
||||||
<meta name="description" content="Portfolio von Niklas - Fotografie, Informatik, Musik und Video." />
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Portfolio von Niklas - Fotografie, Informatik, Musik und Video."
|
||||||
|
/>
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
|||||||
@@ -55,11 +55,10 @@
|
|||||||
<style global>
|
<style global>
|
||||||
.markdown {
|
.markdown {
|
||||||
color: white;
|
color: white;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
.markdown h1 {
|
.markdown h1 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
@@ -80,6 +79,8 @@
|
|||||||
|
|
||||||
.markdown img {
|
.markdown img {
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown table,
|
.markdown table,
|
||||||
|
|||||||
+63
-19
@@ -1,5 +1,44 @@
|
|||||||
const cache = new Map<string, { timestamp: number; data: any }>();
|
import { LRUCache } from "lru-cache";
|
||||||
|
|
||||||
|
// const cache = new Map<string, { timestamp: number; data: any }>();
|
||||||
const DEFAULT_TTL = 1000 * 60 * 15; // 15 minutes default TTL
|
const DEFAULT_TTL = 1000 * 60 * 15; // 15 minutes default TTL
|
||||||
|
const MAX_CACHE_SIZE = 1000 * 1000 * 750; // 750 MB
|
||||||
|
|
||||||
|
const cache = new LRUCache({
|
||||||
|
max: 100,
|
||||||
|
// Maximum allowed size (in bytes)
|
||||||
|
maxSize: MAX_CACHE_SIZE,
|
||||||
|
|
||||||
|
// IMPORTANT: We must teach the cache how to calculate the size of an item.
|
||||||
|
// We check for your specific image structure, generic buffers, or fallback to JSON length.
|
||||||
|
sizeCalculation: (value) => {
|
||||||
|
// 1. Check if it's our specific image object { buffer: ArrayBuffer, ... }
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
const obj = value as Record<string, any>;
|
||||||
|
if (obj.buffer instanceof ArrayBuffer) {
|
||||||
|
return obj.buffer.byteLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Handle raw ArrayBuffers or Node.js Buffers
|
||||||
|
if (value instanceof ArrayBuffer) {
|
||||||
|
return value.byteLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Buffer.isBuffer(value)) {
|
||||||
|
return value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback for strings or JSON metadata
|
||||||
|
try {
|
||||||
|
const str = JSON.stringify(value);
|
||||||
|
return str ? str.length : 1;
|
||||||
|
} catch {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ttl: DEFAULT_TTL,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Caches the result of a promise-returning function.
|
* Caches the result of a promise-returning function.
|
||||||
@@ -7,37 +46,42 @@ const DEFAULT_TTL = 1000 * 60 * 15; // 15 minutes default TTL
|
|||||||
* @param fetcher Function that returns a promise with the data to cache.
|
* @param fetcher Function that returns a promise with the data to cache.
|
||||||
* @param ttl Time to live in milliseconds. Defaults to 15 minutes.
|
* @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>(
|
||||||
const now = Date.now();
|
key: string,
|
||||||
const entry = cache.get(key);
|
fetcher: () => Promise<T>,
|
||||||
|
ttl = DEFAULT_TTL,
|
||||||
|
): Promise<T> {
|
||||||
|
const entry = cache.get(key);
|
||||||
|
|
||||||
if (entry && (now - entry.timestamp < ttl)) {
|
if (entry) {
|
||||||
return entry.data as T;
|
return entry as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetcher();
|
const data = await fetcher();
|
||||||
cache.set(key, { timestamp: now, data });
|
if (data) {
|
||||||
return data;
|
cache.set(key, data, { ttl: ttl });
|
||||||
} 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;
|
|
||||||
}
|
}
|
||||||
|
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.
|
* Manually clears a specific cache key.
|
||||||
*/
|
*/
|
||||||
export function clearCache(key: string) {
|
export function clearCache(key: string) {
|
||||||
cache.delete(key);
|
cache.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the entire cache.
|
* Clears the entire cache.
|
||||||
*/
|
*/
|
||||||
export function clearAllCache() {
|
export function clearAllCache() {
|
||||||
cache.clear();
|
cache.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-31
@@ -1,40 +1,49 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "../app.css";
|
import "../app.css";
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from "svelte";
|
||||||
import { ParaglideJS } from '@inlang/paraglide-sveltekit';
|
import { ParaglideJS } from "@inlang/paraglide-sveltekit";
|
||||||
import { i18n } from '$lib/i18n';
|
import { i18n } from "$lib/i18n";
|
||||||
import { languageTag } from '$lib/paraglide/runtime';
|
import { languageTag } from "$lib/paraglide/runtime";
|
||||||
import * as m from '$lib/paraglide/messages';
|
import { page } from "$app/state";
|
||||||
|
import * as m from "$lib/paraglide/messages";
|
||||||
|
|
||||||
let { children }: { children?: Snippet } = $props();
|
let { children }: { children?: Snippet } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{m.site_title()}</title>
|
<title>{m.site_title()}</title>
|
||||||
<meta name="description" content={m.site_description()} />
|
<meta name="description" content={m.site_description()} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<ParaglideJS {i18n}>
|
<ParaglideJS {i18n}>
|
||||||
<header class="fixed z-50 mix-blend-difference p-1 w-full flex justify-between items-start">
|
<header
|
||||||
<a href="/" class="text-white text-2xl">schreifuchs.ch</a>
|
class="fixed z-50 mix-blend-difference p-1 w-full flex justify-between items-start"
|
||||||
<nav class="flex gap-2 p-2">
|
>
|
||||||
<a
|
<a href={i18n.resolveRoute("/")} class="text-white text-2xl"
|
||||||
href="/"
|
>schreifuchs.ch</a
|
||||||
hreflang="de"
|
>
|
||||||
data-no-translate
|
<nav class="flex gap-2 p-2">
|
||||||
class="text-white hover:underline {languageTag() === 'de' ? 'font-bold' : ''}"
|
<a
|
||||||
>
|
href={i18n.resolveRoute(i18n.route(page.url.pathname), "de")}
|
||||||
DE
|
hreflang="de"
|
||||||
</a>
|
data-no-translate
|
||||||
<a
|
class="text-white hover:underline {languageTag() === 'de'
|
||||||
href="/en"
|
? 'font-bold'
|
||||||
hreflang="en"
|
: ''}"
|
||||||
data-no-translate
|
>
|
||||||
class="text-white hover:underline {languageTag() === 'en' ? 'font-bold' : ''}"
|
DE
|
||||||
>
|
</a>
|
||||||
EN
|
<a
|
||||||
</a>
|
href={i18n.resolveRoute(i18n.route(page.url.pathname), "en")}
|
||||||
</nav>
|
hreflang="en"
|
||||||
</header>
|
data-no-translate
|
||||||
{@render children?.()}
|
class="text-white hover:underline {languageTag() === 'en'
|
||||||
|
? 'font-bold'
|
||||||
|
: ''}"
|
||||||
|
>
|
||||||
|
EN
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
{@render children?.()}
|
||||||
</ParaglideJS>
|
</ParaglideJS>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
// Helper to resolve image source: Local override -> Nextcloud cover -> Placeholder (handled by ImageTile)
|
// Helper to resolve image source: Local override -> Nextcloud cover -> Placeholder (handled by ImageTile)
|
||||||
const getTileImage = (page: { slug: string; coverImage: string | null }) => {
|
const getTileImage = (page: { slug: string; coverImage: string | null }) => {
|
||||||
return staticImages[page.slug.toLowerCase()] || page.coverImage || "";
|
return page.coverImage || staticImages[page.slug.toLowerCase()] || "";
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -46,4 +46,5 @@
|
|||||||
{m.pfadi()}
|
{m.pfadi()}
|
||||||
</h2>
|
</h2>
|
||||||
</ImageLinkTile>
|
</ImageLinkTile>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -1,101 +1,136 @@
|
|||||||
|
// src/routes/assets/[...path]/+server.ts
|
||||||
import { client } from "$lib/server/nextcloud";
|
import { client } from "$lib/server/nextcloud";
|
||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import type { RequestHandler } from "./$types";
|
import type { RequestHandler } from "./$types";
|
||||||
import mime from "mime-types";
|
import mime from "mime-types";
|
||||||
import { env } from "$env/dynamic/private";
|
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 }) => {
|
export const GET: RequestHandler = async ({ params, url }) => {
|
||||||
const { path } = params;
|
const { path } = params;
|
||||||
const width = url.searchParams.get("w");
|
const width = url.searchParams.get("w");
|
||||||
const height = url.searchParams.get("h");
|
const height = url.searchParams.get("h");
|
||||||
|
|
||||||
if (!path) {
|
|
||||||
throw error(400, "Path is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
if (!path) {
|
||||||
let buffer: ArrayBuffer;
|
throw error(400, "Path is required");
|
||||||
let contentType: string;
|
}
|
||||||
|
|
||||||
|
// 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<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];
|
const pathsToTry = [path];
|
||||||
if (!path.startsWith('_')) {
|
if (!path.startsWith("_")) {
|
||||||
pathsToTry.push(`_${path}`);
|
pathsToTry.push(`_${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let response: Response | null = null;
|
|
||||||
let lastError: any = null;
|
let lastError: any = null;
|
||||||
|
|
||||||
for (const currentPath of pathsToTry) {
|
for (const currentPath of pathsToTry) {
|
||||||
try {
|
try {
|
||||||
if (width || height) {
|
// A. Try Nextcloud Preview (Resizing)
|
||||||
// Use Nextcloud Preview API
|
if (width || height) {
|
||||||
const baseUrl = env.NEXTCLOUD_URL?.endsWith("/") ? env.NEXTCLOUD_URL : `${env.NEXTCLOUD_URL}/`;
|
const baseUrl = env.NEXTCLOUD_URL?.endsWith("/")
|
||||||
|
? env.NEXTCLOUD_URL
|
||||||
let fullPath = currentPath;
|
: `${env.NEXTCLOUD_URL}/`;
|
||||||
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);
|
let fullPath = currentPath;
|
||||||
const previewExtension = isJpg ? 'jpg' : 'png';
|
if (env.NEXTCLOUD_BASE_DIR) {
|
||||||
const previewUrl = new URL(`${baseUrl}index.php/core/preview.${previewExtension}`);
|
const baseDir = env.NEXTCLOUD_BASE_DIR.replace(
|
||||||
// Ensure leading slash for the file parameter
|
/^\/+|\/+$/g,
|
||||||
const fileParam = fullPath.startsWith('/') ? fullPath : `/${fullPath}`;
|
"",
|
||||||
previewUrl.searchParams.set("file", fileParam);
|
);
|
||||||
if (width) previewUrl.searchParams.set("x", width);
|
fullPath = baseDir ? `${baseDir}/${currentPath}` : currentPath;
|
||||||
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 isJpg = fullPath.match(/\.(jpg|jpeg)$/i);
|
||||||
|
const previewExtension = isJpg ? "jpg" : "png";
|
||||||
console.log(`Fetching preview: ${previewUrl.toString()} for file: ${fullPath}`);
|
const previewUrl = new URL(
|
||||||
|
`${baseUrl}index.php/core/preview.${previewExtension}`,
|
||||||
|
);
|
||||||
|
|
||||||
const ncResponse = await fetch(previewUrl.toString(), {
|
const fileParam = fullPath.startsWith("/")
|
||||||
headers: {
|
? fullPath
|
||||||
"Authorization": `Basic ${auth}`
|
: `/${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) {
|
const auth = Buffer.from(
|
||||||
buffer = await ncResponse.arrayBuffer();
|
`${env.NEXTCLOUD_USER}:${env.NEXTCLOUD_PASSWORD}`,
|
||||||
contentType = ncResponse.headers.get("Content-Type") || "image/png";
|
).toString("base64");
|
||||||
|
|
||||||
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}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch original via WebDAV (Fallback or no resize requested)
|
console.log(
|
||||||
console.log(`Fetching original file via WebDAV: /${currentPath}`);
|
`Fetching preview: ${previewUrl.toString()} for file: ${fullPath}`,
|
||||||
const data = await client.getFileContents("/" + currentPath);
|
);
|
||||||
buffer = data as ArrayBuffer;
|
|
||||||
contentType = mime.lookup(currentPath) || "application/octet-stream";
|
const ncResponse = await fetch(previewUrl.toString(), {
|
||||||
|
headers: { Authorization: `Basic ${auth}` },
|
||||||
return new Response(buffer, {
|
});
|
||||||
headers: {
|
|
||||||
"Content-Type": contentType,
|
if (ncResponse.ok) {
|
||||||
"Cache-Control": "public, max-age=86400",
|
const buffer = await ncResponse.arrayBuffer();
|
||||||
"Vary": "Accept-Encoding"
|
const contentType =
|
||||||
}
|
ncResponse.headers.get("Content-Type") || "image/png";
|
||||||
});
|
|
||||||
} catch (e) {
|
// Return the raw data to be cached
|
||||||
lastError = e;
|
return { buffer, contentType };
|
||||||
continue; // Try next path variant
|
} 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");
|
throw lastError || new Error("Asset not found");
|
||||||
} catch (e) {
|
// --- END OF FETCH LOGIC ---
|
||||||
console.error(`Error fetching asset from Nextcloud: ${path}`, e);
|
},
|
||||||
throw error(404, "Asset not found");
|
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");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user