Naturalised env import

This commit is contained in:
Alex Rennie-Lis
2026-06-28 23:21:59 +01:00
parent bed8de789e
commit 0cfd200ac5
3 changed files with 32 additions and 41 deletions

View File

@@ -1,30 +1,27 @@
// src/routes/api/download/+server.js
import { redirect, error } from '@sveltejs/kit'; import { redirect, error } from '@sveltejs/kit';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { import { env } from '$env/dynamic/private';
B2_ENDPOINT, B2_REGION, B2_ACCESS_KEY_ID, B2_SECRET_ACCESS_KEY, B2_BUCKET_NAME
} from '$env/dynamic/private';
const s3 = new S3Client({
endpoint: B2_ENDPOINT,
region: B2_REGION,
credentials: {
accessKeyId: B2_ACCESS_KEY_ID,
secretAccessKey: B2_SECRET_ACCESS_KEY
}
});
export async function GET({ url }) { export async function GET({ url }) {
const id = url.searchParams.get('id'); const id = url.searchParams.get('id');
if (!id) throw error(400, 'Missing photo identity ID'); if (!id) throw error(400, 'Missing photo identity ID');
try { try {
const s3 = new S3Client({
endpoint: env.B2_ENDPOINT,
region: env.B2_REGION,
credentials: {
accessKeyId: env.B2_ACCESS_KEY_ID,
secretAccessKey: env.B2_SECRET_ACCESS_KEY
}
});
const fullSizeKey = `images/${id}`; const fullSizeKey = `images/${id}`;
// Create a command that forces the browser to treat this as an attachment download // Force the browser to treat this as an attachment download
const command = new GetObjectCommand({ const command = new GetObjectCommand({
Bucket: B2_BUCKET_NAME, Bucket: env.B2_BUCKET_NAME,
Key: fullSizeKey, Key: fullSizeKey,
ResponseContentDisposition: `attachment; filename="${id}"` ResponseContentDisposition: `attachment; filename="${id}"`
}); });
@@ -32,12 +29,10 @@ export async function GET({ url }) {
// Generate a quick 60-second valid link // Generate a quick 60-second valid link
const downloadUrl = await getSignedUrl(s3, command, { expiresIn: 60 }); const downloadUrl = await getSignedUrl(s3, command, { expiresIn: 60 });
// Redirect the user directly to the freshly generated download link // Redirect browser directly to the download stream
throw redirect(307, downloadUrl); throw redirect(307, downloadUrl);
} catch (err) { } catch (err) {
// SvelteKit uses redirect exceptions, so let them pass through cleanly
if (err.status === 307) throw err; if (err.status === 307) throw err;
console.error('Server download redirection failed:', err); console.error('Server download redirection failed:', err);
throw error(500, 'Could not generate download routing asset.'); throw error(500, 'Could not generate download routing asset.');
} }

View File

@@ -1,48 +1,46 @@
import { json, error } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit';
import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3'; import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { import { env } from '$env/dynamic/private';
B2_ENDPOINT, B2_REGION, B2_ACCESS_KEY_ID, B2_SECRET_ACCESS_KEY, B2_BUCKET_NAME
} from '$env/dynamic/private';
const s3 = new S3Client({
endpoint: B2_ENDPOINT,
region: B2_REGION,
credentials: {
accessKeyId: B2_ACCESS_KEY_ID,
secretAccessKey: B2_SECRET_ACCESS_KEY
}
});
export async function GET({ url }) { export async function GET({ url }) {
const nextToken = url.searchParams.get('next') || undefined; const nextToken = url.searchParams.get('next') || undefined;
try { try {
// 1. Fetch a batch of files from the thumbs/ folder const s3 = new S3Client({
endpoint: env.B2_ENDPOINT,
region: env.B2_REGION,
credentials: {
accessKeyId: env.B2_ACCESS_KEY_ID,
secretAccessKey: env.B2_SECRET_ACCESS_KEY
}
});
// Fetch a batch of files from the thumbs/ folder
const listCommand = new ListObjectsV2Command({ const listCommand = new ListObjectsV2Command({
Bucket: B2_BUCKET_NAME, Bucket: env.B2_BUCKET_NAME,
Prefix: 'thumbs/', Prefix: 'thumbs/',
MaxKeys: 40, // Increased slight headroom since we filter on the server side now MaxKeys: 40,
ContinuationToken: nextToken ContinuationToken: nextToken
}); });
const listResponse = await s3.send(listCommand); const listResponse = await s3.send(listCommand);
const contents = listResponse.Contents || []; const contents = listResponse.Contents || [];
// 2. Strict Filter: Only allow actual .jpg or .jpeg extensions // Strict Filter: Only allow actual .jpg or .jpeg extensions
const imageFiles = contents.filter(file => { const imageFiles = contents.filter(file => {
const lowerKey = file.Key.toLowerCase(); const lowerKey = file.Key.toLowerCase();
return lowerKey.endsWith('.jpg') || lowerKey.endsWith('.jpeg'); return lowerKey.endsWith('.jpg') || lowerKey.endsWith('.jpeg');
}); });
// 3. Generate presigned URLs for validated items // Generate presigned URLs for validated items
const photos = await Promise.all( const photos = await Promise.all(
imageFiles.map(async (file) => { imageFiles.map(async (file) => {
const id = file.Key.replace('thumbs/', ''); const id = file.Key.replace('thumbs/', '');
const fullSizeKey = `images/${id}`; const fullSizeKey = `images/${id}`;
const thumbCommand = new GetObjectCommand({ Bucket: B2_BUCKET_NAME, Key: file.Key }); const thumbCommand = new GetObjectCommand({ Bucket: env.B2_BUCKET_NAME, Key: file.Key });
const fullCommand = new GetObjectCommand({ Bucket: B2_BUCKET_NAME, Key: fullSizeKey }); const fullCommand = new GetObjectCommand({ Bucket: env.B2_BUCKET_NAME, Key: fullSizeKey });
// 10 minutes = 600 seconds // 10 minutes = 600 seconds
const [thumbUrl, fullUrl] = await Promise.all([ const [thumbUrl, fullUrl] = await Promise.all([
@@ -56,7 +54,6 @@ export async function GET({ url }) {
return json({ return json({
photos, photos,
// Pass the original continuation token back so pagination structural flow remains intact
nextContinuationToken: listResponse.NextContinuationToken || null nextContinuationToken: listResponse.NextContinuationToken || null
}); });
} catch (err) { } catch (err) {

View File

@@ -1,13 +1,12 @@
// src/routes/login/+page.server.js
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import { GALLERY_PASSWORD } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
export const actions = { export const actions = {
default: async ({ request, cookies }) => { default: async ({ request, cookies }) => {
const data = await request.formData(); const data = await request.formData();
const password = data.get('password'); const password = data.get('password');
if (password === GALLERY_PASSWORD) { if (password === env.GALLERY_PASSWORD) {
cookies.set('gallery_session', 'authenticated', { cookies.set('gallery_session', 'authenticated', {
path: '/', path: '/',
httpOnly: true, httpOnly: true,