Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ vite.config.ts.timestamp-*
#lock files
pnpm-lock.yaml
package-lock.json
yarn.lock
yarn.lock

#deploy
script
logs
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v22.13.1
25 changes: 25 additions & 0 deletions config/patrick115.eu
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
server {
listen 80;
listen [::]:80;

server_name patrick115.eu;

return 301 https://$host$request_uri;

}

server {

listen [::]:443 ssl http2;
listen 443 ssl http2;

server_name patrick115.eu;

access_log /opt/NodeApps/Web/logs/access.log;
error_log /opt/NodeApps/Web/logs/error.log;

location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_pass "http://127.0.0.1:5178";
}
}
1 change: 1 addition & 0 deletions config/web.service
4 changes: 4 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
onlyBuiltDependencies:
- bcrypt
- esbuild
- sharp
2 changes: 1 addition & 1 deletion src/components/admin/gallery.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

<div class="flex min-h-32 flex-row content-center gap-2 overflow-x-scroll rounded-md bg-primary p-2">
{#each images as image}
<img class="my-auto h-max max-h-96 w-max" src={image} alt="gallery item" />
<img class="my-auto h-max max-h-96 w-max" src={image} alt="gallery item" loading="lazy" />
{/each}
</div>
2 changes: 1 addition & 1 deletion src/components/galleryItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
href={admin ? `/admin/gallery/${data.id}` : `/customImages/gallery/${data.name}?format=jpeg`}
target={admin ? undefined : '_blank'}
>
<img class="h-auto w-full" src="/customImages/gallery/{data.name}?format=jpeg&scale=50" alt={data.alt} />
<img class="h-auto w-full" src="/customImages/gallery/{data.name}?format=jpeg&scale=50" alt={data.alt} loading="lazy" />
<div class="flex flex-col p-2">
<h2 class="font-ubuntu font-bold lg:text-xl xl:text-2xl">
{data.alt}
Expand Down
66 changes: 52 additions & 14 deletions src/routes/customImages/[...name]/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import sharp from 'sharp';

const formats = ['jpg', 'jpeg', 'png', 'webp', 'tiff'];

export const GET = (async ({ params, setHeaders, url }) => {
// Cache duration: 1 year in seconds
const CACHE_MAX_AGE = 31536000;

export const GET = (async ({ params, url }) => {
if (params.name == null) {
throw error(404, 'Not found');
}
Expand All @@ -18,8 +21,6 @@ export const GET = (async ({ params, setHeaders, url }) => {
throw error(404, 'Not found');
}

let file = fs.readFileSync(filePath);

const searchParams = url.searchParams;
let fileExtension = path.extname(filePath);
let modified = false;
Expand Down Expand Up @@ -60,7 +61,8 @@ export const GET = (async ({ params, setHeaders, url }) => {
const cachePath = path.join('.cache', cacheModifiedName);

if (!fs.existsSync(cachePath)) {
let image = sharp(file);
// Use streaming to process the image
let image = sharp(filePath);

const imageOptions: sharp.JpegOptions & sharp.PngOptions & sharp.WebpOptions & sharp.TiffOptions = {
quality: 75
Expand Down Expand Up @@ -92,23 +94,59 @@ export const GET = (async ({ params, setHeaders, url }) => {
height: newHeight
});

const imageBuffer = await image.toBuffer();

fs.writeFileSync(cachePath, imageBuffer);
await image.toFile(cachePath);
}

file = fs.readFileSync(cachePath);
filePath = cachePath;
}

const fileInfo = fs.statSync(filePath);
const contentType = 'image/' + (fileExtension.startsWith('.') ? fileExtension.slice(1) : fileExtension);

// Create a readable stream for efficient memory usage
const stream = fs.createReadStream(filePath);

// Track cleanup function for proper resource management
let cleanup: (() => void) | null = null;

// Convert Node.js readable stream to web ReadableStream
const webStream = new ReadableStream<Buffer>({
start(controller) {
const onData = (chunk: Buffer) => {
controller.enqueue(chunk);
};
const onEnd = () => {
if (cleanup) cleanup();
controller.close();
};
const onError = (err: Error) => {
if (cleanup) cleanup();
controller.error(err);
};

cleanup = () => {
stream.off('data', onData);
stream.off('end', onEnd);
stream.off('error', onError);
};

setHeaders({
'Content-Type': 'image/' + fileExtension.startsWith('.') ? fileExtension.slice(1) : fileExtension,
'Content-Length': fileInfo.size.toString(),
'Last-Modified': fileInfo.mtime.toUTCString(),
'Cache-Control': 'public, max-age=86400'
stream.on('data', onData);
stream.on('end', onEnd);
stream.on('error', onError);
},
cancel() {
if (cleanup) cleanup();
stream.destroy();
}
});

return new Response(file);
// Return response with headers set early and streaming body
return new Response(webStream, {
headers: {
'Content-Type': contentType,
'Content-Length': fileInfo.size.toString(),
'Last-Modified': fileInfo.mtime.toUTCString(),
'Cache-Control': `public, max-age=${CACHE_MAX_AGE}, immutable`
}
});
}) satisfies RequestHandler;